95 lines
3.8 KiB
TypeScript
95 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|