Files
core2026/app/game-api/test/gameClock.test.ts
T

98 lines
3.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { DatabaseClient } from '../src/context.js';
import { loadCurrentGameTime } from '../src/services/gameClock.js';
const buildDatabase = (
mode: 'realtime' | 'manual' = 'realtime',
phase: 'PREOPEN' | 'RUNNING' | 'MANUAL' = mode === 'manual' ? 'MANUAL' : 'PREOPEN'
): DatabaseClient =>
({
worldState: {
findFirst: vi.fn(async () => ({
clockBaseTime: new Date('2026-08-21T11:00:00.000Z'),
clockTick: 0n,
clockMode: mode,
clockWallAnchor: new Date('2026-08-21T11:00:00.000Z'),
tickSeconds: 600,
clockPhase: phase,
clockRevision: 1n,
deadlineGeneration: 1n,
})),
},
}) as unknown as DatabaseClient;
describe('current game time projection', () => {
it('keeps independent workers frozen until this clock revision has a ready daemon', async () => {
const row = {
clockBaseTime: new Date('2026-09-06T00:00:00Z'),
clockTick: 0n,
clockMode: 'realtime',
clockWallAnchor: new Date('2026-09-06T00:00:00Z'),
tickSeconds: 3600,
clockPhase: 'RUNNING',
clockRevision: 2n,
deadlineGeneration: 2n,
clockRecoveryStartTick: 0n,
clockRecoveryEndTick: 288_000_000n,
clockRecoveryStartWallAt: new Date('2026-09-06T04:00:00Z'),
};
let ready = false;
const db = {
worldState: { findFirst: vi.fn(async () => row) },
$queryRaw: vi.fn(async () => [{ ready }]),
} as unknown as DatabaseClient;
const now = new Date('2026-09-06T05:00:00Z');
expect(await loadCurrentGameTime(db, now)).toMatchObject({ tick: 0, running: false, runtimeReady: false });
ready = true;
expect(await loadCurrentGameTime(db, now)).toMatchObject({
tick: 72_000_000,
running: true,
runtimeReady: true,
recovery: { startsAt: '2026-09-06T04:00:00.000Z', endsAt: '2026-09-06T08:00:00.000Z' },
});
});
it('holds an invader restart until its future turn boundary', async () => {
const db = buildDatabase('realtime', 'RUNNING');
const result = await loadCurrentGameTime(db, new Date('2026-08-21T10:59:59Z'));
expect(result).toMatchObject({ tick: 0, running: false, startsAt: new Date('2026-08-21T11:00:00Z') });
});
it('projects negative realtime ticks until the future opening anchor', async () => {
const db = buildDatabase();
const preopen = await loadCurrentGameTime(db, new Date('2026-08-21T10:30:00.000Z'));
expect(preopen).toMatchObject({
now: new Date('2026-08-21T10:30:00.000Z'),
wallNow: new Date('2026-08-21T10:30:00.000Z'),
tick: -108_000_000,
mode: 'realtime',
phase: 'PREOPEN',
running: false,
startsAt: new Date('2026-08-21T11:00:00.000Z'),
});
const opened = await loadCurrentGameTime(
buildDatabase('realtime', 'RUNNING'),
new Date('2026-08-21T11:00:05.000Z')
);
expect(opened).toMatchObject({
now: new Date('2026-08-21T11:00:05.000Z'),
tick: 300_000,
running: true,
startsAt: null,
});
});
it('keeps a manual clock stopped without scheduling an automatic start', async () => {
const result = await loadCurrentGameTime(buildDatabase('manual'), new Date('2026-08-21T12:00:00.000Z'));
expect(result).toMatchObject({
now: new Date('2026-08-21T11:00:00.000Z'),
tick: 0,
running: false,
startsAt: null,
});
});
});