merge: 최신 main을 게임 버전 표시에 통합

This commit is contained in:
2026-08-21 01:28:58 +00:00
17 changed files with 765 additions and 66 deletions
@@ -700,6 +700,63 @@ describe('admin operation API', () => {
});
});
it('keeps reset start, preopen, and formal open as an ordered lifecycle', async () => {
const harness = await buildCaller(async (input) => ({
id: '77777777-7777-4777-8777-777777777777',
profileName: input.profileName,
type: 'RESET',
status: 'QUEUED',
sourceMode: input.sourceMode,
sourceRef: input.sourceRef,
payload: input.payload ?? {},
requestedBy: input.requestedBy,
scheduledAt: input.scheduledAt,
createdAt: '2026-08-08T00:00:00.000Z',
updatedAt: '2026-08-08T00:00:00.000Z',
}));
const install = {
scenarioId: 1010,
turnTermMinutes: 60,
sync: false,
fiction: 1 as const,
extend: false,
blockGeneralCreate: 0 as const,
npcMode: 0 as const,
showImgLevel: 0 as const,
tournamentTrig: false,
joinMode: 'full' as const,
preopenAt: '2099-01-01T01:00:00.000Z',
openAt: '2099-01-01T02:00:00.000Z',
};
await harness.caller.admin.operations.requestReset({
profileName: 'che:2',
sourceMode: 'COMMIT',
sourceRef: 'HEAD',
scheduledAt: '2099-01-01T00:00:00.000Z',
install,
});
expect(harness.createdInputs[0]).toMatchObject({
type: 'RESET',
scheduledAt: '2099-01-01T00:00:00.000Z',
payload: { install },
});
await expect(
harness.caller.admin.operations.requestReset({
profileName: 'che:2',
sourceMode: 'COMMIT',
sourceRef: 'HEAD',
scheduledAt: '2099-01-01T01:30:00.000Z',
install,
})
).rejects.toMatchObject({
code: 'BAD_REQUEST',
message: 'preopenAt cannot be earlier than scheduledAt.',
});
});
it('returns validated profile reset defaults to a scenario-only operator', async () => {
const harness = await buildCaller(
async () => {
@@ -48,6 +48,9 @@ const createHarness = (
startGate?: Promise<void>,
options: {
profile?: GatewayProfileRecord;
profiles?: GatewayProfileRecord[];
reservedToStart?: GatewayProfileRecord[];
now?: () => Date;
cancelGame?: GatewayOrchestratorOptions['cancelGame'];
} = {}
) => {
@@ -59,10 +62,11 @@ const createHarness = (
const started: ProcessDefinition[] = [];
const stopped: string[] = [];
const deleted: string[] = [];
const buildStatuses: string[] = [];
const logs: Array<{ phase: string; message: string; level: string }> = [];
const repository: GatewayProfileRepository = {
listProfiles: async () => [harnessProfile],
listProfiles: async () => options.profiles ?? [harnessProfile],
getProfile: async () => harnessProfile,
upsertProfile: async () => harnessProfile,
updateCurrentScenario: async () => harnessProfile,
@@ -70,9 +74,12 @@ const createHarness = (
statuses.push(status);
return { ...harnessProfile, status };
},
updateBuildStatus: async () => harnessProfile,
updateBuildStatus: async (_profileName, status) => {
buildStatuses.push(status);
return { ...harnessProfile, buildStatus: status };
},
updateMeta: async () => harnessProfile,
listReservedToStart: async () => [],
listReservedToStart: async () => options.reservedToStart ?? [],
findQueuedBuild: async () => null,
updateLastError: async () => {},
updateWorkspaceUsage: async () => {},
@@ -167,10 +174,11 @@ const createHarness = (
scheduleIntervalMs: 60_000,
buildIntervalMs: 60_000,
adminActionIntervalMs: 60_000,
now: options.now,
cancelGame: options.cancelGame,
});
return { orchestrator, statuses, completions, completionFields, started, stopped, deleted, logs };
return { orchestrator, statuses, buildStatuses, completions, completionFields, started, stopped, deleted, logs };
};
describe('GatewayOrchestrator first-class operations', () => {
@@ -267,6 +275,81 @@ describe('GatewayOrchestrator first-class operations', () => {
expect(harness.deleted).toEqual([]);
});
it('opens a prepared reserved profile without rebuilding it again', async () => {
const now = new Date('2030-01-01T01:00:00.000Z');
const reservedProfile: GatewayProfileRecord = {
...profile,
status: 'RESERVED',
currentScenario: '1010',
scenario: '1010',
buildStatus: 'SUCCEEDED',
buildWorkspace: '/srv/sammo/worktrees/0123456789abcdef',
preopenAt: now.toISOString(),
openAt: '2030-01-01T02:00:00.000Z',
};
const harness = createHarness(buildOperation('START'), false, false, false, false, undefined, undefined, {
profile: reservedProfile,
profiles: [],
reservedToStart: [reservedProfile],
now: () => now,
});
await harness.orchestrator.runScheduleNow();
expect(harness.statuses).toEqual(['PREOPEN']);
expect(harness.buildStatuses).toEqual([]);
});
it('starts turns when a prepared reserved profile is handled after formal open', async () => {
const now = new Date('2030-01-01T02:00:00.000Z');
const reservedProfile: GatewayProfileRecord = {
...profile,
status: 'RESERVED',
currentScenario: '1010',
scenario: '1010',
buildStatus: 'SUCCEEDED',
buildWorkspace: '/srv/sammo/worktrees/0123456789abcdef',
preopenAt: '2030-01-01T01:00:00.000Z',
openAt: now.toISOString(),
};
const harness = createHarness(buildOperation('START'), false, false, false, false, undefined, undefined, {
profile: reservedProfile,
profiles: [],
reservedToStart: [reservedProfile],
now: () => now,
});
await harness.orchestrator.runScheduleNow();
expect(harness.statuses).toEqual(['RUNNING']);
expect(harness.buildStatuses).toEqual([]);
});
it('retains the legacy build queue for an unprepared reserved profile', async () => {
const now = new Date('2030-01-01T01:00:00.000Z');
const reservedProfile: GatewayProfileRecord = {
...profile,
status: 'RESERVED',
currentScenario: null,
scenario: 'default',
buildStatus: 'IDLE',
buildWorkspace: undefined,
preopenAt: now.toISOString(),
openAt: '2030-01-01T02:00:00.000Z',
};
const harness = createHarness(buildOperation('START'), false, false, false, false, undefined, undefined, {
profile: reservedProfile,
profiles: [],
reservedToStart: [reservedProfile],
now: () => now,
});
await harness.orchestrator.runScheduleNow();
expect(harness.statuses).toEqual([]);
expect(harness.buildStatuses).toEqual(['QUEUED']);
});
it('starts every profile process and records success', async () => {
const harness = createHarness(buildOperation('START'));
@@ -8,6 +8,7 @@ import {
buildProcessDefinitions,
buildWorkspaceCommands,
planProfileReconcile,
resolveResetLifecycleStatus,
} from '../src/orchestrator/gatewayOrchestrator.js';
import { sanitizeManagedProcessEnv } from '../src/orchestrator/processManager.js';
import type { GatewayProfileRecord } from '../src/orchestrator/profileRepository.js';
@@ -108,6 +109,29 @@ describe('planProfileReconcile', () => {
});
});
describe('resolveResetLifecycleStatus', () => {
const now = new Date('2030-01-01T00:00:00.000Z');
it('keeps an initialized profile reserved until the configured preopen time', () => {
expect(
resolveResetLifecycleStatus(now, new Date('2030-01-01T01:00:00.000Z'), new Date('2030-01-01T02:00:00.000Z'))
).toBe('RESERVED');
});
it('moves through preopen before the formal open time', () => {
expect(
resolveResetLifecycleStatus(now, new Date('2029-12-31T23:00:00.000Z'), new Date('2030-01-01T02:00:00.000Z'))
).toBe('PREOPEN');
});
it('runs immediately when no future lifecycle boundary remains', () => {
expect(resolveResetLifecycleStatus(now, null, null)).toBe('RUNNING');
expect(
resolveResetLifecycleStatus(now, new Date('2029-12-31T22:00:00.000Z'), new Date('2029-12-31T23:00:00.000Z'))
).toBe('RUNNING');
});
});
describe('buildProcessDefinitions', () => {
const processConfig = {
workspaceRoot: '/srv/sammo/main',