feat: add logical game clock

This commit is contained in:
2026-08-04 02:51:27 +00:00
parent 87965a39d6
commit a26031dc3f
51 changed files with 1605 additions and 398 deletions
+28 -1
View File
@@ -14,7 +14,11 @@ const buildRedis = () => ({
const buildDb = (options: {
updated: number;
auction?: { status: 'OPEN' | 'FINALIZING' | 'FINISHED' | 'CANCELED'; closeAt: Date } | null;
auction?: {
status: 'OPEN' | 'FINALIZING' | 'FINISHED' | 'CANCELED';
closeAt: Date;
closeTick?: bigint | null;
} | null;
existingEvents?: Array<{
requestId: string;
target: 'ENGINE';
@@ -69,6 +73,29 @@ describe('auction worker clock-shift race', () => {
expect(transaction.inputEvent.create).not.toHaveBeenCalled();
});
it('requeues a tick-backed auction using its logical deadline score', async () => {
const redis = buildRedis();
const closeAt = new Date('2099-01-01T00:00:00.000Z');
const { db } = buildDb({
updated: 0,
auction: { status: 'OPEN', closeAt, closeTick: 72_000_000n },
});
await expect(
processDueAuctionId({
db,
redis,
timerKey: 'timer',
historyKey: 'history',
id: '7',
nowMs: new Date('2042-01-01T00:00:00.000Z').getTime(),
nowTick: 36_000_000,
})
).resolves.toBe('RESCHEDULED');
expect(redis.zAdd).toHaveBeenCalledWith('timer', [{ score: 72_000_000, value: '7' }]);
});
it('commits the FINALIZING transition and durable command in one transaction before recording history', async () => {
const redis = buildRedis();
const closeAt = new Date('2026-07-30T11:00:00.000Z');
@@ -220,6 +220,25 @@ describe('tournament worker schedule compatibility', () => {
expect(resolveNextAt(state)).toBe('2026-08-02T10:10:00.000Z');
expect(resolveBettingCloseAt(state)).toBe('2026-08-02T11:00:00.000Z');
});
it('uses the injected game clock, not the host wall clock, to close betting', async () => {
const redis = new MemoryRedis();
const store = new TournamentStore(redis, buildTournamentKeys('game-clock-deadline'));
const bettingCloseAt = '2099-01-01T00:00:00.000Z';
const state = createTournamentState({ stage: 6, bettingCloseAt, nextAt: bettingCloseAt });
await store.setState(state);
const next = await applyPreBattleStage(
store,
createPrismaMock({ baseSeed: 'clock-seed' }),
state,
'clock-seed',
createNoopDaemonTransport(),
() => new Date('2100-01-01T00:00:00.000Z').getTime()
);
expect(next).toMatchObject({ stage: 7, bettingCloseAt });
});
});
describe('tournament worker (in-memory)', () => {