feat: 실행 중 게임 옵션 변경을 지원
Gateway 관리 화면에서 현재 기수의 장수 생성 제한, 유저 자동턴, 턴 간격을 내구성 action으로 변경한다. 턴 간격 변경은 논리 tick과 현재 게임 시각을 보존하며 DB와 Redis의 tick 기반 시각을 재투영하고 기존 로그 timestamp는 유지한다.
This commit is contained in:
@@ -287,6 +287,14 @@ const buildCaller = async (
|
||||
}
|
||||
},
|
||||
cleanupStaleWorkspaces: async () => ({ removed: [], skipped: [] }),
|
||||
listRuntimeSettings: async () => [
|
||||
{
|
||||
profileName: 'che:2',
|
||||
turnTermMinutes: 20,
|
||||
blockGeneralCreate: 2,
|
||||
autorunUser: { limitMinutes: 720, options: ['develop', 'recruit_high', 'chief'] },
|
||||
},
|
||||
],
|
||||
listRuntimeStates: async () => {
|
||||
runtimeStateListCount += 1;
|
||||
return [];
|
||||
@@ -299,6 +307,7 @@ const buildCaller = async (
|
||||
findFirst: async () => ({ id: options.firstUserIsAdmin === false ? 'bootstrap-user' : admin.id }),
|
||||
},
|
||||
gatewayRuntimeAction: {
|
||||
findMany: async () => [],
|
||||
create: async ({ data }: { data: Record<string, unknown> }) => {
|
||||
if (options.runtimeActionCreateError) {
|
||||
throw options.runtimeActionCreateError;
|
||||
@@ -317,6 +326,9 @@ const buildCaller = async (
|
||||
};
|
||||
},
|
||||
},
|
||||
gatewayOperation: {
|
||||
findMany: async () => [],
|
||||
},
|
||||
systemSetting: {
|
||||
findUnique: async () => ({ id: 1, notice: storedNotice }),
|
||||
upsert: async ({ create, update }: { create: { notice: string }; update: { notice: string } }) => {
|
||||
@@ -371,6 +383,32 @@ describe('admin profile navigation API', () => {
|
||||
]);
|
||||
expect(harness.getRuntimeStateListCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('returns live settings from the profile database separately from reset defaults', async () => {
|
||||
const harness = await buildCaller(
|
||||
async () => {
|
||||
throw new Error('not used');
|
||||
},
|
||||
{
|
||||
profileMeta: {
|
||||
resetDefaults: {
|
||||
turnTermMinutes: 60,
|
||||
blockGeneralCreate: 0,
|
||||
autorunUser: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await harness.caller.admin.profiles.list();
|
||||
|
||||
expect(result[0]?.runtimeSettings).toEqual({
|
||||
profileName: 'che:2',
|
||||
turnTermMinutes: 20,
|
||||
blockGeneralCreate: 2,
|
||||
autorunUser: { limitMinutes: 720, options: ['develop', 'recruit_high', 'chief'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin scenario catalog API', () => {
|
||||
@@ -1131,6 +1169,7 @@ describe('admin runtime clock action API', () => {
|
||||
{
|
||||
profileName: 'che:2',
|
||||
action: 'ACCELERATE',
|
||||
payload: {},
|
||||
durationMinutes: 15,
|
||||
reason: '운영 일정 조정',
|
||||
requestedBy: harness.admin.id,
|
||||
@@ -1151,10 +1190,78 @@ describe('admin runtime clock action API', () => {
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
code: 'CONFLICT',
|
||||
message: '이 프로필의 이전 시간 조정 요청이 아직 처리 중입니다.',
|
||||
message: '이 프로필의 이전 런타임 변경 요청이 아직 처리 중입니다.',
|
||||
});
|
||||
});
|
||||
|
||||
it('queues all live game settings as one durable runtime action', async () => {
|
||||
const harness = await buildCaller(unusedCreateOperation, { initialProfileStatus: 'RUNNING' });
|
||||
|
||||
const result = await harness.caller.admin.profiles.requestAction({
|
||||
profileName: 'che:2',
|
||||
action: 'UPDATE_RUNTIME_SETTINGS',
|
||||
runtimeSettings: {
|
||||
turnTermMinutes: 20,
|
||||
blockGeneralCreate: 2,
|
||||
autorunUser: {
|
||||
limitMinutes: 720,
|
||||
options: ['develop', 'recruit_high', 'chief'],
|
||||
},
|
||||
},
|
||||
reason: '운영 중 규칙 변경',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
action: { action: 'UPDATE_RUNTIME_SETTINGS', status: 'REQUESTED' },
|
||||
});
|
||||
expect(harness.createdRuntimeActions).toEqual([
|
||||
{
|
||||
profileName: 'che:2',
|
||||
action: 'UPDATE_RUNTIME_SETTINGS',
|
||||
payload: {
|
||||
settings: {
|
||||
turnTermMinutes: 20,
|
||||
blockGeneralCreate: 2,
|
||||
autorunUser: {
|
||||
limitMinutes: 720,
|
||||
options: ['develop', 'recruit_high', 'chief'],
|
||||
},
|
||||
},
|
||||
},
|
||||
durationMinutes: undefined,
|
||||
reason: '운영 중 규칙 변경',
|
||||
requestedBy: harness.admin.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects a live game setting change without a reason or running database', async () => {
|
||||
const running = await buildCaller(unusedCreateOperation, { initialProfileStatus: 'RUNNING' });
|
||||
await expect(
|
||||
running.caller.admin.profiles.requestAction({
|
||||
profileName: 'che:2',
|
||||
action: 'UPDATE_RUNTIME_SETTINGS',
|
||||
runtimeSettings: { turnTermMinutes: 20 },
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'BAD_REQUEST', message: '변경 사유를 입력해 주세요.' });
|
||||
|
||||
const stopped = await buildCaller(unusedCreateOperation, { initialProfileStatus: 'STOPPED' });
|
||||
await expect(
|
||||
stopped.caller.admin.profiles.requestAction({
|
||||
profileName: 'che:2',
|
||||
action: 'UPDATE_RUNTIME_SETTINGS',
|
||||
runtimeSettings: { blockGeneralCreate: 1 },
|
||||
reason: '운영 정책 변경',
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
code: 'BAD_REQUEST',
|
||||
message: '실행 중인 프로필에서만 현재 기수 설정을 바꿀 수 있습니다.',
|
||||
});
|
||||
expect(running.createdRuntimeActions).toEqual([]);
|
||||
expect(stopped.createdRuntimeActions).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects a scheduled clock shift instead of silently applying it immediately', async () => {
|
||||
const harness = await buildCaller(unusedCreateOperation);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user