fix: 게임 시계 완료 감사 경계 보강

This commit is contained in:
2026-09-03 11:14:01 +00:00
parent c10a71fbcc
commit 24a63d338c
13 changed files with 208 additions and 24 deletions
@@ -283,6 +283,7 @@ integration('adjustGeneralIcon PostgreSQL persistence', () => {
command: Extract<TurnDaemonCommand, { type: 'adjustGeneralIcon' }>,
actorUserId = command.userId
): Promise<void> => {
const clock = world.getGameClockState();
await db.inputEvent.create({
data: {
requestId: command.requestId!,
@@ -294,6 +295,12 @@ integration('adjustGeneralIcon PostgreSQL persistence', () => {
leaseUntil: new Date('2026-07-31T09:30:00.000Z'),
attempts: 1,
payload: command as GamePrisma.InputJsonValue,
acceptedGameTick: BigInt(clock.tick),
acceptedClockRevision: BigInt(clock.revision),
acceptedDeadlineGeneration: BigInt(clock.deadlineGeneration),
processingGameTick: BigInt(clock.tick),
processingClockRevision: BigInt(clock.revision),
processingDeadlineGeneration: BigInt(clock.deadlineGeneration),
},
});
};
@@ -263,6 +263,7 @@ integration('inheritance action PostgreSQL atomic persistence', () => {
const createInputEvent = async (
command: Extract<TurnDaemonCommand, { type: 'inheritanceAction' }>
): Promise<void> => {
const clock = world.getGameClockState();
await db.inputEvent.create({
data: {
requestId: command.requestId!,
@@ -274,6 +275,12 @@ integration('inheritance action PostgreSQL atomic persistence', () => {
leaseUntil: new Date('2026-08-24T01:00:00.000Z'),
attempts: 1,
payload: command as GamePrisma.InputJsonValue,
acceptedGameTick: BigInt(clock.tick),
acceptedClockRevision: BigInt(clock.revision),
acceptedDeadlineGeneration: BigInt(clock.deadlineGeneration),
processingGameTick: BigInt(clock.tick),
processingClockRevision: BigInt(clock.revision),
processingDeadlineGeneration: BigInt(clock.deadlineGeneration),
},
});
};
@@ -99,9 +99,7 @@ integration('game-engine read-model journal PostgreSQL transaction', () => {
afterAll(async () => {
await hooks?.close();
if (db) {
await db.$executeRawUnsafe(
`ALTER TABLE read_model_outbox DROP CONSTRAINT IF EXISTS ${rollbackConstraint}`
);
await db.$executeRawUnsafe(`ALTER TABLE read_model_outbox DROP CONSTRAINT IF EXISTS ${rollbackConstraint}`);
await db.inputEvent.deleteMany({ where: { requestId } });
await db.logEntry.deleteMany({ where: { text: { startsWith: '[read-model-journal]' } } });
await db.worldState.deleteMany({ where: { id: worldId } });
@@ -187,6 +185,7 @@ integration('game-engine read-model journal PostgreSQL transaction', () => {
currentMonth: 2,
});
const commandClock = world.getGameClockState();
await db.inputEvent.create({
data: {
requestId,
@@ -194,6 +193,12 @@ integration('game-engine read-model journal PostgreSQL transaction', () => {
eventType: 'shiftSchedule',
status: 'PROCESSING',
payload: {},
acceptedGameTick: BigInt(commandClock.tick),
acceptedClockRevision: BigInt(commandClock.revision),
acceptedDeadlineGeneration: BigInt(commandClock.deadlineGeneration),
processingGameTick: BigInt(commandClock.tick),
processingClockRevision: BigInt(commandClock.revision),
processingDeadlineGeneration: BigInt(commandClock.deadlineGeneration),
},
});
const directResult: TurnDaemonCommandResult = {
@@ -250,9 +255,9 @@ integration('game-engine read-model journal PostgreSQL transaction', () => {
{ domain: 'records.history', entityId: 0, revision: 1n },
]);
await expect(db.readModelOutbox.count()).resolves.toBe(3);
await expect(db.logEntry.count({ where: { text: { startsWith: '[read-model-journal] direct' } } })).resolves.toBe(
3
);
await expect(
db.logEntry.count({ where: { text: { startsWith: '[read-model-journal] direct' } } })
).resolves.toBe(3);
world.updateWorldMeta({ queueProbe: 1 });
await hooks.hooks.flushChanges?.(turnRunResult(world));
@@ -174,6 +174,7 @@ describe('HWE-shaped unification invader resume', () => {
const world = new InMemoryTurnWorld(state, snapshot, {
schedule: { entries: [{ startMinute: 0, tickMinutes: 1 }] },
});
const addGeneral = vi.spyOn(world, 'addGeneral');
const reservedTurns = new InMemoryReservedTurnStore(
{
generalTurn: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn() },
@@ -301,6 +302,14 @@ describe('HWE-shaped unification invader resume', () => {
});
expect(world.listNations().filter((entry) => entry.name.startsWith('ⓞ'))).toHaveLength(1);
expect(world.listGenerals().filter((entry) => entry.npcState === 9)).toHaveLength(10);
const initialInvaderTurnTimes = addGeneral.mock.calls
.map(([general]) => general)
.filter((general) => general.npcState === 9)
.map((general) => general.turnTime.getTime());
const alignedMonthlyBoundary = addMinutes(liveLastTurnTime, 3).getTime();
expect(initialInvaderTurnTimes).toHaveLength(10);
expect(Math.min(...initialInvaderTurnTimes)).toBeGreaterThanOrEqual(alignedMonthlyBoundary);
expect(Math.max(...initialInvaderTurnTimes)).toBeLessThan(alignedMonthlyBoundary + 60_000);
expect(run).toHaveBeenCalledTimes(2);
expect(run.mock.calls.map(([targetTime]) => targetTime.toISOString())).toEqual([
'2026-08-20T06:24:58.611Z',