감사 정책 head 유실을 복구하고 데몬 준비 상태로 배포를 검증한다

This commit is contained in:
2026-09-26 15:21:17 +00:00
parent 11d0a4afdd
commit daf7a87799
14 changed files with 323 additions and 31 deletions
+16 -1
View File
@@ -1,9 +1,24 @@
import { describe, expect, it, vi } from 'vitest';
import type { DatabaseClient } from '../src/context.js';
import { loadClockAdminStatus, loadClockReadiness } from '../src/services/clockReadiness.js';
import { loadClockAdminStatus, loadClockReadiness, loadProfileReadiness } from '../src/services/clockReadiness.js';
describe('clock reconciliation readiness', () => {
it.each([false, true])('requires the profile daemon lease to be ready: %s', async (ready) => {
const db = {
worldState: {
findFirst: vi.fn(async () => ({ clockPhase: 'PREOPEN', clockRevision: 1n, deadlineGeneration: 0n })),
},
clockProjectionOutbox: { count: vi.fn(async () => 0) },
$queryRaw: vi.fn(async () => [{ ready }]),
} as unknown as DatabaseClient;
await expect(loadProfileReadiness(db, 'che:default')).resolves.toMatchObject({
ok: ready,
daemonReady: ready,
clock: { reconciliationComplete: true, gameplayEnabled: false },
});
});
it('fails closed when the reconciliation schema is not available', async () => {
const db = {} as DatabaseClient;
await expect(loadClockReadiness(db)).resolves.toEqual({
@@ -0,0 +1,62 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
import { loadProfileReadiness } from '../src/services/clockReadiness.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
const integration = describe.skipIf(!databaseUrl);
integration('profile deployment readiness', () => {
let db: GamePrismaClient;
let close: () => Promise<void>;
beforeAll(async () => {
const connector = createGamePostgresConnector({ url: databaseUrl! });
await connector.connect();
db = connector.prisma;
close = () => connector.disconnect();
});
afterAll(async () => {
await close?.();
});
it('rejects absent, initializing, expired and other-profile leases; accepts ready paused/preopen runtime', async () => {
const rollback = new Error('readiness fixture rollback');
await expect(
db.$transaction(async (tx) => {
// Shared integration fixtures may leave projection work; rollback restores it.
await tx.clockProjectionOutbox.deleteMany();
await tx.worldState.create({
data: {
id: -998901,
scenarioCode: 'readiness-fixture',
currentYear: 190,
currentMonth: 1,
tickSeconds: 300,
clockPhase: 'PREOPEN',
config: {},
meta: {},
},
});
const profile = 'readiness:fixture';
expect((await loadProfileReadiness(tx, profile)).ok).toBe(false);
await tx.turnDaemonLease.create({
data: {
profile,
ownerId: 'fixture',
fencingEpoch: 1n,
heartbeatAt: new Date(),
leaseUntil: new Date(Date.now() + 60_000),
clockReady: false,
},
});
expect((await loadProfileReadiness(tx, profile)).ok).toBe(false);
await tx.turnDaemonLease.update({ where: { profile }, data: { clockReady: true } });
expect((await loadProfileReadiness(tx, profile)).ok).toBe(true);
expect((await loadProfileReadiness(tx, 'readiness:other')).ok).toBe(false);
await tx.turnDaemonLease.update({
where: { profile },
data: { leaseUntil: new Date(Date.now() - 60_000) },
});
expect((await loadProfileReadiness(tx, profile)).ok).toBe(false);
throw rollback;
})
).rejects.toBe(rollback);
});
});