feat(gateway): 프로필별 첫 기수 번호를 관리한다

첫 기수 번호를 완료 게임 수의 기준값으로 적용하고 0기를 API와 화면에서 보존한다. 시즌 번호와 독립된 RESET 계약을 관리 패널, 테스트, 운영 문서에 반영한다.
This commit is contained in:
2026-08-21 13:49:11 +00:00
parent 32d69d7049
commit 88be40f729
15 changed files with 192 additions and 19 deletions
@@ -877,6 +877,30 @@ describe('admin operation API', () => {
).rejects.toBeDefined();
});
it('stores zero as the first game index and rejects negative values', async () => {
const harness = await buildCaller(
async () => {
throw new Error('not used');
},
{ adminRoles: ['admin.profiles.settings:che:2'], firstUserIsAdmin: false }
);
await harness.caller.admin.profiles.updateMeta({
profileName: 'che:2',
patch: { firstGameIdx: 0 },
reason: 'start core series at zero',
});
expect(harness.updatedMetas.at(-1)).toMatchObject({ firstGameIdx: 0 });
await expect(
harness.caller.admin.profiles.updateMeta({
profileName: 'che:2',
patch: { firstGameIdx: -1 },
reason: 'reject negative game index',
})
).rejects.toBeDefined();
});
it('does not let a scenario-only operator combine a Git update with reset', async () => {
const harness = await buildCaller(
async () => {
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';
import { resolveProfileFirstGameIdx } from '../src/orchestrator/gatewayOrchestrator.js';
describe('profile first game index', () => {
it('preserves an explicitly configured zero', () => {
expect(resolveProfileFirstGameIdx({ firstGameIdx: 0 })).toBe(0);
});
it('defaults missing or invalid metadata to one', () => {
expect(resolveProfileFirstGameIdx({})).toBe(1);
expect(resolveProfileFirstGameIdx({ firstGameIdx: -1 })).toBe(1);
expect(resolveProfileFirstGameIdx({ firstGameIdx: 0.5 })).toBe(1);
expect(resolveProfileFirstGameIdx({ firstGameIdx: 'invalid' })).toBe(1);
});
});
@@ -41,6 +41,8 @@ describeDatabase('selected workspace profile seed CLI', () => {
const requestFile = path.join(tempDirectory, 'request.json');
const connector = createGamePostgresConnector({ url: databaseUrl });
try {
await connector.connect();
const completedGameCount = await connector.prisma.gameHistory.count({ where: { status: 'COMPLETED' } });
await fs.writeFile(
requestFile,
JSON.stringify({
@@ -49,6 +51,7 @@ describeDatabase('selected workspace profile seed CLI', () => {
now: '2036-03-03T00:00:00.000Z',
installOptions: {
serverId: 'selected-cli-seed',
firstGameIdx: 0,
installOperationId: 'selected-cli-operation',
installCommitSha: 'selected-cli-commit',
},
@@ -63,11 +66,12 @@ describeDatabase('selected workspace profile seed CLI', () => {
const result = await runSeedCli(requestFile);
expect(result, result.output).toMatchObject({ code: 0 });
await connector.connect();
const world = await connector.prisma.worldState.findFirstOrThrow();
expect(world).toMatchObject({
scenarioCode: '1010',
meta: {
firstGameIdx: 0,
gameIdx: completedGameCount,
installOperationId: 'selected-cli-operation',
installCommitSha: 'selected-cli-commit',
},