Files
core2026/app/game-engine/test/clockReconciliationRetry.test.ts
T

72 lines
2.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { GamePrismaClient } from '@sammo-ts/infra';
import {
reconcileClockSuspension,
startClockSuspension,
type ClockReconciliationResult,
type ClockSuspensionResult,
} from '../src/turn/clockReconciliation.js';
const authority = { kind: 'OFFLINE' as const, profileName: 'retry-test', reason: 'isolated unit test' };
describe('serializable clock operation retries', () => {
it('retries a PostgreSQL serialization conflict while starting suspension', async () => {
const result: ClockSuspensionResult = {
suspensionId: 'retry-start',
phase: 'SUSPENDED',
sourceRevision: 7,
targetRevision: 8,
cutTick: 123,
cutWallAt: new Date('2026-09-03T10:00:00.000Z'),
};
const transaction = vi
.fn()
.mockRejectedValueOnce(Object.assign(new Error('could not serialize access'), { code: 'P2034' }))
.mockResolvedValueOnce(result);
const db = { $transaction: transaction } as unknown as GamePrismaClient;
await expect(
startClockSuspension({ db, suspensionId: result.suspensionId, source: 'MAINTENANCE', authority })
).resolves.toEqual(result);
expect(transaction).toHaveBeenCalledTimes(2);
});
it('retries a raw 40001 conflict while reconciling suspension', async () => {
const result: ClockReconciliationResult = {
suspensionId: 'retry-resume',
phase: 'RECONCILING',
sourceRevision: 7,
targetRevision: 8,
deadlineGeneration: 8,
gapTicks: 100,
catchUpTicks: 0,
shiftTicks: 100,
alignedTick: 223,
resumeWallAt: new Date('2026-09-03T10:10:00.000Z'),
};
const transaction = vi
.fn()
.mockRejectedValueOnce(
Object.assign(new Error('SQLSTATE 40001'), { code: 'P2010', meta: { code: '40001' } })
)
.mockResolvedValueOnce(result);
const db = { $transaction: transaction } as unknown as GamePrismaClient;
await expect(reconcileClockSuspension({ db, suspensionId: result.suspensionId, authority })).resolves.toEqual(
result
);
expect(transaction).toHaveBeenCalledTimes(2);
});
it('does not retry a non-serialization failure', async () => {
const transaction = vi.fn().mockRejectedValue(new Error('authority denied'));
const db = { $transaction: transaction } as unknown as GamePrismaClient;
await expect(
startClockSuspension({ db, suspensionId: 'no-retry', source: 'MAINTENANCE', authority })
).rejects.toThrow('authority denied');
expect(transaction).toHaveBeenCalledTimes(1);
});
});