fix(gateway): split profile identity from scenario

This commit is contained in:
2026-08-13 16:03:41 +00:00
parent 826d17da81
commit 6478a9f478
26 changed files with 399 additions and 83 deletions
+5 -1
View File
@@ -85,6 +85,8 @@ const buildCaller = async (
const profile = {
profileName: 'che:2',
profile: 'che',
instanceKey: '2',
currentScenario: options.profileScenario ?? '2',
scenario: options.profileScenario ?? '2',
apiPort: 15003,
status: options.initialProfileStatus ?? ('STOPPED' as const),
@@ -98,7 +100,7 @@ const buildCaller = async (
listProfiles: async () => [profile],
getProfile: async () => profile,
upsertProfile: async () => profile,
updateScenario: async () => profile,
updateCurrentScenario: async () => profile,
updateStatus: async (_profileName, status) => {
updatedStatuses.push(status);
return { ...profile, status };
@@ -362,6 +364,8 @@ describe('admin profile navigation API', () => {
{
profileName: 'che:2',
profile: 'che',
instanceKey: '2',
currentScenario: '2',
meta: {},
},
]);
@@ -15,6 +15,8 @@ import { appRouter } from '../src/router.js';
const profile = {
profileName: 'che:default',
profile: 'che',
instanceKey: 'default',
currentScenario: null,
scenario: 'default',
apiPort: 15003,
status: 'RUNNING' as const,
@@ -28,7 +30,7 @@ const profiles: GatewayProfileRepository = {
listProfiles: async () => [profile],
getProfile: async (profileName) => (profileName === profile.profileName ? profile : null),
upsertProfile: async () => profile,
updateScenario: async () => profile,
updateCurrentScenario: async () => profile,
updateStatus: async () => profile,
updateBuildStatus: async () => profile,
updateMeta: async () => profile,
+7 -1
View File
@@ -92,6 +92,8 @@ const buildCaller = (
{
profileName: 'che:default',
profile: 'che',
instanceKey: 'default',
currentScenario: null,
scenario: 'default',
apiPort: 15003,
status: 'RUNNING' as const,
@@ -103,6 +105,8 @@ const buildCaller = (
{
profileName: 'hwe:default',
profile: 'hwe',
instanceKey: 'default',
currentScenario: null,
scenario: 'default',
apiPort: 15015,
status: 'RUNNING' as const,
@@ -119,7 +123,7 @@ const buildCaller = (
upsertProfile: async () => {
throw new Error('not used');
},
updateScenario: async () => null,
updateCurrentScenario: async () => null,
updateStatus: async () => null,
updateBuildStatus: async () => null,
updateMeta: async () => null,
@@ -167,6 +171,8 @@ const buildCaller = (
profileRows.map((profile) => ({
profileName: profile.profileName,
profile: profile.profile,
instanceKey: profile.instanceKey,
currentScenario: profile.currentScenario,
scenario: profile.scenario,
status: profile.status,
apiPort: profile.apiPort,
@@ -13,6 +13,8 @@ import { GitWorkspaceManager } from '../src/orchestrator/workspaceManager.js';
const profile: GatewayProfileRecord = {
profileName: 'che:2',
profile: 'che',
instanceKey: '2',
currentScenario: '2',
scenario: '2',
apiPort: 15003,
status: 'STOPPED',
@@ -58,7 +60,7 @@ const createHarness = (
listProfiles: async () => [profile],
getProfile: async () => profile,
upsertProfile: async () => profile,
updateScenario: async () => profile,
updateCurrentScenario: async () => profile,
updateStatus: async (_profileName, status) => {
statuses.push(status);
return { ...profile, status };
@@ -14,6 +14,8 @@ import type { GatewayProfileRecord } from '../src/orchestrator/profileRepository
const buildProfile = (buildWorkspace?: string): GatewayProfileRecord => ({
profileName: 'che:2',
profile: 'che',
instanceKey: '2',
currentScenario: '2',
scenario: '2',
apiPort: 15003,
status: 'RUNNING',
@@ -172,6 +174,39 @@ describe('buildProcessDefinitions', () => {
expect(definitions.tournament.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-api'));
});
it('keeps the instance identity stable while passing the mutable current scenario', () => {
const definitions = buildProcessDefinitions(
{
...buildProfile(),
profileName: 'che:default',
instanceKey: 'default',
currentScenario: '1010',
scenario: '1010',
},
processConfig
);
expect(definitions.api.name).toBe('sammo:che:default:game-api');
expect(definitions.api.env).toMatchObject({
GAME_PROFILE_NAME: 'che:default',
SCENARIO: '1010',
});
expect(definitions.daemon.env).toMatchObject({
TURN_PROFILE_NAME: 'che:default',
SCENARIO: '1010',
});
});
it('uses the legacy default scenario marker only for an uninitialized instance runtime', () => {
const definitions = buildProcessDefinitions(
{ ...buildProfile(), currentScenario: null, scenario: 'default' },
processConfig
);
expect(definitions.api.env.SCENARIO).toBe('default');
expect(definitions.daemon.env.SCENARIO).toBe('default');
});
it('does not forward PM2 identity or parent runtime roles to profile processes', () => {
const definitions = buildProcessDefinitions(buildProfile(), {
...processConfig,
@@ -14,6 +14,8 @@ const makeProfile = (
): GatewayProfileRecord => ({
profileName,
profile: profileName.split(':')[0] ?? 'che',
instanceKey: profileName.split(':')[1] ?? 'default',
currentScenario: null,
scenario: profileName.split(':')[1] ?? 'default',
apiPort: 15_003,
status: 'RUNNING',
@@ -50,6 +50,8 @@ describe('profile DEPLOY operation', () => {
const profile: GatewayProfileRecord = {
profileName: 'che:1010',
profile: 'che',
instanceKey: '1010',
currentScenario: '1010',
scenario: '1010',
apiPort: 15003,
status: 'RUNNING',
@@ -80,7 +82,7 @@ describe('profile DEPLOY operation', () => {
listProfiles: async () => [profile],
getProfile: async () => profile,
upsertProfile: async () => profile,
updateScenario: async () => profile,
updateCurrentScenario: async () => profile,
updateStatus: async () => profile,
updateBuildStatus: async () => profile,
updateMeta: async () => profile,
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import {
buildGatewayProfileName,
resolveGatewayProfileIdentity,
type GatewayProfileUpsertInput,
} from '../src/orchestrator/profileRepository.js';
const resolve = (input: Partial<GatewayProfileUpsertInput>) =>
resolveGatewayProfileIdentity({
profile: 'che',
apiPort: 15003,
...input,
});
describe('Gateway profile identity', () => {
it('builds the immutable technical id from profile and instance key', () => {
expect(buildGatewayProfileName('che', 'default')).toBe('che:default');
});
it('does not treat a new default instance as an initialized scenario', () => {
expect(resolve({ instanceKey: 'default' })).toEqual({
instanceKey: 'default',
currentScenario: null,
shouldUpdateCurrentScenario: false,
});
});
it('accepts the old bootstrap default marker without clearing an existing scenario on upsert', () => {
expect(resolve({ scenario: 'default' })).toEqual({
instanceKey: 'default',
currentScenario: null,
shouldUpdateCurrentScenario: false,
});
});
it('maps a legacy non-default scenario to both identity and current state', () => {
expect(resolve({ scenario: '2' })).toEqual({
instanceKey: '2',
currentScenario: '2',
shouldUpdateCurrentScenario: true,
});
});
it('keeps a default instance stable when its current scenario changes', () => {
expect(resolve({ instanceKey: 'default', currentScenario: '1010' })).toEqual({
instanceKey: 'default',
currentScenario: '1010',
shouldUpdateCurrentScenario: true,
});
});
});
+10 -10
View File
@@ -6,25 +6,25 @@ describe('orderGatewayProfiles', () => {
it('uses the public server order instead of alphabetical profile order', () => {
const profiles = ['hwe', 'pya', 'che', 'nya', 'twe', 'pwe', 'kwe'].map((profile) => ({
profile,
scenario: 'default',
instanceKey: 'default',
}));
expect(orderGatewayProfiles(profiles).map(({ profile }) => profile)).toEqual(GATEWAY_PROFILE_ORDER);
});
it('orders scenarios within a profile and places unknown profiles afterward', () => {
it('orders instance keys within a profile and places unknown profiles afterward', () => {
const profiles = [
{ profile: 'zeta', scenario: 'default' },
{ profile: 'che', scenario: '20' },
{ profile: 'alpha', scenario: 'default' },
{ profile: 'che', scenario: '10' },
{ profile: 'zeta', instanceKey: 'default' },
{ profile: 'che', instanceKey: '20' },
{ profile: 'alpha', instanceKey: 'default' },
{ profile: 'che', instanceKey: '10' },
];
expect(orderGatewayProfiles(profiles)).toEqual([
{ profile: 'che', scenario: '10' },
{ profile: 'che', scenario: '20' },
{ profile: 'alpha', scenario: 'default' },
{ profile: 'zeta', scenario: 'default' },
{ profile: 'che', instanceKey: '10' },
{ profile: 'che', instanceKey: '20' },
{ profile: 'alpha', instanceKey: 'default' },
{ profile: 'zeta', instanceKey: 'default' },
]);
expect(profiles[0]?.profile).toBe('zeta');
});
+1 -1
View File
@@ -38,7 +38,7 @@ describe('readReleaseManifest', () => {
await expect(readReleaseManifest(workspaceRoot)).resolves.toMatchObject({
controllerProtocol: RELEASE_CONTROLLER_PROTOCOL,
gatewaySchemaHead: '20260811000000_add_gateway_operation_logs',
gatewaySchemaHead: '20260813000000_split_gateway_profile_identity',
gameSchemaHead: '20260803000000_add_logical_game_clock',
});
});