test reserved turn daemon api race

This commit is contained in:
2026-07-27 10:19:10 +00:00
parent 26e033665a
commit 0a01ccef0a
@@ -0,0 +1,94 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import type { DatabaseClient } from '@sammo-ts/game-api/context.js';
import {
ReservedTurnRevisionConflictError,
getGeneralTurnSnapshot,
setGeneralTurn,
} from '@sammo-ts/game-api/turns/reservedTurns.js';
import { InMemoryReservedTurnStore } from '@sammo-ts/game-engine/turn/reservedTurnStore.js';
import { createGamePostgresConnector } from '@sammo-ts/infra';
const databaseUrl = process.env.RESERVED_TURN_DATABASE_URL;
const describeIntegration = databaseUrl ? describe : describe.skip;
const GENERAL_ID = 2_147_400_010;
describeIntegration('reserved turn daemon/API stale-cache race', () => {
const connector = databaseUrl ? createGamePostgresConnector({ url: databaseUrl }) : null;
beforeAll(async () => {
if (!connector) {
return;
}
await connector.connect();
await connector.prisma.generalTurn.deleteMany({ where: { generalId: GENERAL_ID } });
await connector.prisma.generalTurnRevision.deleteMany({ where: { generalId: GENERAL_ID } });
await connector.prisma.generalTurn.createMany({
data: [
{ generalId: GENERAL_ID, turnIdx: 0, actionCode: 'che_훈련', arg: {} },
{ generalId: GENERAL_ID, turnIdx: 1, actionCode: '휴식', arg: {} },
],
});
await connector.prisma.generalTurnRevision.create({
data: { generalId: GENERAL_ID, revision: 0 },
});
});
afterAll(async () => {
if (!connector) {
return;
}
await connector.prisma.generalTurn.deleteMany({ where: { generalId: GENERAL_ID } });
await connector.prisma.generalTurnRevision.deleteMany({ where: { generalId: GENERAL_ID } });
await connector.disconnect();
});
it('refreshes an already-loaded daemon queue after an API write and serializes later writers', async () => {
if (!connector) {
throw new Error('integration connector is unavailable');
}
const store = new InMemoryReservedTurnStore(connector.prisma, {
maxGeneralTurns: 30,
maxNationTurns: 12,
leaseOwner: 'integration-stale-cache-daemon',
leaseDurationMs: 60_000,
});
await store.loadAll();
expect(store.getGeneralTurn(GENERAL_ID, 1).action).toBe('휴식');
const apiWrite = await connector.prisma.$transaction((transaction) =>
setGeneralTurn(transaction as unknown as DatabaseClient, GENERAL_ID, 1, 'che_화계', { destCityId: 7 }, 0)
);
expect(apiWrite.revision).toBe(1);
await store.prepareTurnsForExecution(GENERAL_ID);
expect(store.getGeneralTurn(GENERAL_ID, 1)).toEqual({
action: 'che_화계',
args: { destCityId: 7 },
});
await expect(
connector.prisma.$transaction((transaction) =>
setGeneralTurn(transaction as unknown as DatabaseClient, GENERAL_ID, 2, 'che_사기진작', {}, 1)
)
).rejects.toBeInstanceOf(ReservedTurnRevisionConflictError);
store.shiftGeneralTurns(GENERAL_ID, -1);
const changes = store.peekDirtyState();
await connector.prisma.$transaction((transaction) => store.persistChanges(transaction, changes));
store.acknowledgeDirtyState(changes);
const afterDaemon = await getGeneralTurnSnapshot(connector.prisma as unknown as DatabaseClient, GENERAL_ID);
expect(afterDaemon.revision).toBe(2);
expect(afterDaemon.turns[0]).toMatchObject({
action: 'che_화계',
args: { destCityId: 7 },
});
const retry = await connector.prisma.$transaction((transaction) =>
setGeneralTurn(transaction as unknown as DatabaseClient, GENERAL_ID, 2, 'che_사기진작', {}, 2)
);
expect(retry.revision).toBe(3);
});
});