feat: 오프너의 천하통일 서버 정리를 허용한다

시나리오 초기화 권한 범위에서 실제 game DB의 통일 상태를 재검증한 뒤 완료 서버만 STOPPED로 정리한다. 런타임 전체 권한은 확대하지 않고 DB와 완료 기록을 보존한다.
This commit is contained in:
2026-08-24 01:55:31 +00:00
parent 32bab36320
commit f8fb570dac
6 changed files with 203 additions and 12 deletions
@@ -31,6 +31,7 @@ const buildCaller = async (
initialProfileStatus?: GatewayProfileRecord['status'];
profileScenario?: string | null;
profileMeta?: GatewayProfileRecord['meta'];
gameIsUnited?: number;
releaseCommitSha?: string;
initialOperation?: GatewayOperationRecord;
profileLogVisibilityAfterPolls?: number;
@@ -291,6 +292,7 @@ const buildCaller = async (
listRuntimeSettings: async () => [
{
profileName: 'che:2',
isUnited: options.gameIsUnited ?? 0,
turnTermMinutes: 20,
blockGeneralCreate: 2,
autorunUser: { limitMinutes: 720, options: ['develop', 'recruit_high', 'chief'] },
@@ -405,6 +407,7 @@ describe('admin profile navigation API', () => {
expect(result[0]?.runtimeSettings).toEqual({
profileName: 'che:2',
isUnited: 0,
turnTermMinutes: 20,
blockGeneralCreate: 2,
autorunUser: { limitMinutes: 720, options: ['develop', 'recruit_high', 'chief'] },
@@ -1427,6 +1430,63 @@ describe('admin runtime clock action API', () => {
expect(harness.getReconcileCount()).toBe(1);
});
it('lets a scoped scenario opener close a unified game without runtime authority', async () => {
const harness = await buildCaller(unusedCreateOperation, {
adminRoles: ['user', 'admin.scenarios.reset:che:2'],
firstUserIsAdmin: false,
initialProfileStatus: 'RUNNING',
gameIsUnited: 2,
});
await expect(
harness.caller.admin.profiles.requestAction({
profileName: 'che:2',
action: 'CLOSE_COMPLETED',
})
).resolves.toMatchObject({ ok: true });
expect(harness.updatedStatuses).toEqual(['STOPPED']);
expect(harness.getReconcileCount()).toBe(1);
expect(harness.auditEvents.at(-1)).toMatchObject({ capability: 'admin.scenarios.reset' });
});
it('does not let a scenario opener close a game before unification', async () => {
const harness = await buildCaller(unusedCreateOperation, {
adminRoles: ['user', 'admin.scenarios.reset:che:2'],
firstUserIsAdmin: false,
initialProfileStatus: 'RUNNING',
gameIsUnited: 0,
});
await expect(
harness.caller.admin.profiles.requestAction({
profileName: 'che:2',
action: 'CLOSE_COMPLETED',
})
).rejects.toMatchObject({
code: 'BAD_REQUEST',
message: 'Only a unified game can be closed by a scenario opener.',
});
expect(harness.updatedStatuses).toEqual([]);
expect(harness.getReconcileCount()).toBe(0);
});
it('does not turn runtime authority into scenario-opener cleanup authority', async () => {
const harness = await buildCaller(unusedCreateOperation, {
adminRoles: ['user', 'admin.profiles.runtime:che:2'],
firstUserIsAdmin: false,
initialProfileStatus: 'RUNNING',
gameIsUnited: 2,
});
await expect(
harness.caller.admin.profiles.requestAction({
profileName: 'che:2',
action: 'CLOSE_COMPLETED',
})
).rejects.toMatchObject({ code: 'FORBIDDEN' });
expect(harness.updatedStatuses).toEqual([]);
});
it('creates a first-class clock action owned by the authenticated administrator', async () => {
const harness = await buildCaller(unusedCreateOperation);