Restore reproducible isolated UI fixtures

This commit is contained in:
2026-10-07 20:08:02 -04:00
parent 70185eb98c
commit 5b3e8cb1a3
6 changed files with 632 additions and 1 deletions
+196
View File
@@ -0,0 +1,196 @@
import Database from 'better-sqlite3';
import { access, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { parseFixtureOptions, reserveFixture } from '../../scripts/fixture-options.js';
import { FIXTURE_KEY, FIXTURE_VERSION, refreshFixtureData, seedFixtureData } from '../../scripts/fixture-data.js';
import { newFixture, type Fixture } from '../helpers/fixture.js';
const NOW = Date.parse('2026-10-07T12:00:00Z');
const run = promisify(execFile);
describe('fixture destination isolation', () => {
let root: string;
beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'cherry-fixture-safety-')); });
afterEach(async () => { vi.unstubAllEnvs(); await rm(root, { recursive: true, force: true }); });
it('requires an explicit absolute destination even with a production database in the environment', () => {
vi.stubEnv('DB_PATH', join(root, 'production.db'));
expect(() => parseFixtureOptions([], NOW)).toThrow(/--db/);
expect(() => parseFixtureOptions(['--db', './data/cherry.db'], NOW)).toThrow(/absolute/);
expect(parseFixtureOptions(['--db', join(root, 'preview', 'fixture.db')], NOW))
.toEqual({ dbPath: join(root, 'preview', 'fixture.db'), now: NOW, watch: false });
expect(() => parseFixtureOptions(['--db', join(root, 'preview', 'fixture.db'), '--force'], NOW)).toThrow(/Unknown option/);
});
it('keeps deterministic snapshot time separate from a live watch', () => {
const target = ['--db', join(root, 'preview', 'fixture.db')];
expect(parseFixtureOptions([...target, '--now', '2026-10-07T08:00:00-04:00'], 0).now).toBe(NOW);
expect(() => parseFixtureOptions([...target, '--now', '2026-10-07T12:00:00'], NOW)).toThrow(/timezone/);
expect(() => parseFixtureOptions([...target, '--now', '2026-10-07T12:00:00Z', '--watch'], NOW)).toThrow(/separately/);
expect(() => parseFixtureOptions([...target, '--db', join(root, 'second', 'fixture.db')], NOW)).toThrow(/Duplicate/);
});
it('reserves a private new directory and refuses reuse without touching the database or sidecars', async () => {
const reservation = await reserveFixture(join(root, 'preview', 'fixture.db'));
await writeFile(reservation.dbPath, 'retained database');
await writeFile(`${reservation.dbPath}-wal`, 'retained WAL');
await expect(reserveFixture(reservation.dbPath)).rejects.toThrow(/already exists/);
expect(await readFile(reservation.dbPath, 'utf8')).toBe('retained database');
expect(await readFile(`${reservation.dbPath}-wal`, 'utf8')).toBe('retained WAL');
expect((await stat(reservation.root)).mode & 0o777).toBe(0o700);
expect((await stat(reservation.dbPath)).mode & 0o777).toBe(0o600);
});
it('rejects a pre-existing destination-directory symlink instead of following it', async () => {
const target = await reserveFixture(join(root, 'production', 'database.db'));
await writeFile(target.dbPath, 'do not replace');
await symlink(target.root, join(root, 'preview'), 'dir');
await expect(reserveFixture(join(root, 'preview', 'database.db'))).rejects.toThrow(/already exists/);
expect(await readFile(target.dbPath, 'utf8')).toBe('do not replace');
});
it('runs the real CLI against only the explicit destination despite ambient production paths', async () => {
const ambient = join(root, 'production.db');
await writeFile(ambient, 'original production sentinel');
const destination = join(root, 'preview', 'fixture.db');
const { stdout } = await run(process.execPath, ['--import', 'tsx', 'scripts/seed-fixture.ts', '--db', destination, '--now', '2026-10-07T12:00:00Z'], {
cwd: process.cwd(), timeout: 30_000, maxBuffer: 200_000,
env: { ...process.env, DB_PATH: ambient, SERVERS_ROOT: join(root, 'production-t5'),
T6_SERVERS_ROOT: join(root, 'production-t6'), BETA_ARCHIVE_DIR: join(root, 'production-archive'),
GAME_FILES_ROOT: join(root, 'production-files'), RCON_PASSWORD: 'ambient-secret-must-not-escape' }
});
expect(await readFile(ambient, 'utf8')).toBe('original production sentinel');
await expect(access(join(root, 'production-t5'))).rejects.toThrow();
await expect(access(join(root, 'production-files'))).rejects.toThrow();
expect(stdout).not.toContain('ambient-secret-must-not-escape');
expect(stdout).not.toContain('[worker] started');
const saved = JSON.parse(await readFile(join(root, 'preview', 'fixture.json'), 'utf8'));
expect(saved.dbPath).toBe(await realpath(destination));
expect(saved.password.length).toBeGreaterThanOrEqual(24);
const environment = await readFile(join(root, 'preview', 'preview.env'), 'utf8');
expect(environment).not.toContain('production');
expect(environment).not.toContain('ambient-secret');
const seeded = new Database(destination, { readonly: true, fileMustExist: true });
try {
expect(seeded.pragma('integrity_check', { simple: true })).toBe('ok');
expect(seeded.pragma('foreign_key_check')).toEqual([]);
expect(seeded.prepare('SELECT COUNT(*) n FROM leaderboard_matches').get()).toEqual({ n: 500 });
expect(seeded.prepare("SELECT COUNT(*) n FROM auth_users WHERE username='demo' AND role='admin'").get()).toEqual({ n: 1 });
expect(seeded.prepare("SELECT COUNT(*) n FROM ec_host_operations WHERE state IN ('pending','running','accepted')").get()).toEqual({ n: 0 });
} finally { seeded.close(); }
}, 35_000);
it('cleans up only its newly owned directory when fixture creation cannot complete', async () => {
const retained = join(root, 'keep.txt'); await writeFile(retained, 'keep');
// This filename collides with the generated environment file. Whether
// rejected up front or at write time, no partial preview should remain.
await expect(run(process.execPath, ['--import', 'tsx', 'scripts/seed-fixture.ts', '--db', join(root, 'failed-preview', 'preview.env')], {
cwd: process.cwd(), timeout: 30_000, maxBuffer: 200_000
})).rejects.toThrow();
await expect(access(join(root, 'failed-preview'))).rejects.toThrow();
expect(await readFile(retained, 'utf8')).toBe('keep');
}, 35_000);
});
describe('useful synthetic fixture data', () => {
let fixture: Fixture;
let handle: Database.Database;
beforeAll(async () => {
vi.resetModules(); fixture = await newFixture([]);
const { db } = await import('../../src/lib/server/db/index.js');
const { ensureHostSchema } = await import('../../src/lib/server/hosts/schema.js');
const { ensureAuthSchema } = await import('../../src/lib/server/auth/schema.js');
const { ensureGameFileSchema } = await import('../../src/lib/server/game-files/schema.js');
handle = db(); ensureHostSchema(handle); ensureAuthSchema(handle); ensureGameFileSchema(handle);
seedFixtureData(handle, NOW);
}, 20_000);
beforeEach(() => handle.exec('SAVEPOINT fixture_test'));
afterEach(() => handle.exec('ROLLBACK TO fixture_test; RELEASE fixture_test'));
afterAll(async () => { await fixture?.cleanup(); });
const count = (sql: string) => (handle.prepare(sql).get() as { n: number }).n;
it('provides complete paginated match, fleet and metrics evidence with valid relationships', () => {
expect(handle.pragma('foreign_key_check')).toEqual([]);
expect(count('SELECT COUNT(*) n FROM servers')).toBe(12);
expect(count('SELECT COUNT(*) n FROM ec_hosts')).toBe(3);
expect(count('SELECT COUNT(*) n FROM leaderboard_matches')).toBe(500);
expect(count("SELECT COUNT(*) n FROM leaderboard_matches WHERE status='active'")).toBe(6);
expect(count("SELECT COUNT(*) n FROM leaderboard_matches WHERE status='completed'")).toBeGreaterThan(400);
expect(handle.prepare('SELECT DISTINCT start_players FROM leaderboard_matches WHERE start_players IS NOT NULL ORDER BY start_players').all())
.toEqual([1, 2, 3, 4].map(start_players => ({ start_players })));
expect(count('SELECT COUNT(*) n FROM leaderboard_matches WHERE start_players IS NULL')).toBeGreaterThan(0);
expect(count('SELECT COUNT(DISTINCT guid) n FROM leaderboard_match_players')).toBeGreaterThan(50);
const metrics = handle.prepare('SELECT COUNT(DISTINCT at) samples,MIN(at) oldest,MAX(at) newest FROM metric_samples').get() as { samples: number; oldest: number; newest: number };
expect(metrics.samples).toBe(7 * 24 * 60);
expect(metrics.newest).toBe(NOW);
expect(metrics.newest - metrics.oldest).toBe((metrics.samples - 1) * 60_000);
expect(count('SELECT COUNT(*) n FROM metric_samples WHERE players<0 OR players>capacity')).toBe(0);
});
it('supports actual leaderboard pagination and evidence for every exact squad size', async () => {
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
const first = getLeaderboard({ game: 't5', map: 'theater', players: 2, limit: 25 });
const second = getLeaderboard({ game: 't5', map: 'theater', players: 2, limit: 25, offset: 25 });
expect(first.total).toBeGreaterThan(50);
expect(first.entries).toHaveLength(25);
expect(second.entries).toHaveLength(25);
expect(new Set([...first.entries, ...second.entries].map(entry => entry.guid)).size).toBe(50);
for (const players of [1, 2, 3, 4] as const) {
const board = getLeaderboard({ players });
expect(board.total).toBeGreaterThan(0);
const evidence = board.entries[0].records.highestRound;
expect(handle.prepare('SELECT start_players FROM leaderboard_matches WHERE id=?').get(evidence.matchId)).toEqual({ start_players: players });
}
});
it('never seeds executable work or usable host credentials and pauses automated lifecycle policies', async () => {
expect(count("SELECT COUNT(*) n FROM server_deployments WHERE state IN ('queued','waiting','running','awaiting-health')")).toBe(0);
expect(count("SELECT COUNT(*) n FROM ec_host_operations WHERE state IN ('pending','accepted','running','unknown')")).toBe(0);
expect(count("SELECT COUNT(*) n FROM maintenance_jobs WHERE state IN ('queued','running','waiting')")).toBe(0);
expect(count("SELECT COUNT(*) n FROM beta_servers bs JOIN betas b ON b.id=bs.beta_id WHERE b.state IN ('running','ready-promote')")).toBe(0);
expect(count("SELECT COUNT(*) n FROM deployment_servers WHERE restart_state IN ('pending','failed','held')")).toBe(0);
const { maintenancePolicies } = await import('../../src/lib/server/maintenance/policies.js');
expect(maintenancePolicies()).toEqual({ paused: true, health: false, drift: false, updates: false });
for (const row of handle.prepare('SELECT token_hash FROM ec_host_credentials').all() as { token_hash: string }[])
expect(row.token_hash).not.toMatch(/^[0-9a-f]{64}$/);
expect(() => seedFixtureData(handle, NOW)).toThrow(/new database/);
expect(count('SELECT COUNT(*) n FROM leaderboard_matches')).toBe(500);
});
it('refuses watch updates without the exact ownership marker', () => {
const before = handle.prepare('SELECT name,last_checked_at FROM servers ORDER BY name').all();
handle.prepare('DELETE FROM settings WHERE key=?').run(FIXTURE_KEY);
expect(() => refreshFixtureData(handle, NOW + 60_000)).toThrow(/unmarked/);
handle.prepare('INSERT INTO settings(key,value) VALUES(?,?)').run(FIXTURE_KEY, 'some-other-fixture');
expect(() => refreshFixtureData(handle, NOW + 60_000)).toThrow(/unmarked/);
expect(handle.prepare('SELECT name,last_checked_at FROM servers ORDER BY name').all()).toEqual(before);
});
it('refreshes only owned observations while preserving unrelated resources and retained history', () => {
handle.prepare('INSERT INTO servers(name,last_checked_at) VALUES(?,?)').run('unowned-server', '2020-01-01');
handle.prepare('INSERT INTO ec_hosts(id,name,created_at,last_heartbeat) VALUES(?,?,?,?)').run('unowned-host', 'Keep this host', '2020-01-01', '2020-01-01');
const history = handle.prepare("SELECT id,last_received_at FROM leaderboard_matches WHERE status!='active' ORDER BY id").all();
const operations = handle.prepare('SELECT id,state,updated_at FROM ec_host_operations ORDER BY id').all();
refreshFixtureData(handle, NOW + 60_000);
expect(handle.prepare('SELECT value FROM settings WHERE key=?').get(FIXTURE_KEY)).toEqual({ value: FIXTURE_VERSION });
expect(handle.prepare('SELECT last_checked_at FROM servers WHERE name=?').get('unowned-server')).toEqual({ last_checked_at: '2020-01-01' });
expect(handle.prepare('SELECT last_heartbeat FROM ec_hosts WHERE id=?').get('unowned-host')).toEqual({ last_heartbeat: '2020-01-01' });
expect(handle.prepare("SELECT id,last_received_at FROM leaderboard_matches WHERE status!='active' ORDER BY id").all()).toEqual(history);
expect(handle.prepare('SELECT id,state,updated_at FROM ec_host_operations ORDER BY id').all()).toEqual(operations);
expect(count(`SELECT COUNT(*) n FROM servers WHERE last_checked_at='${new Date(NOW + 60_000).toISOString()}'`)).toBe(12);
});
it('restores synthetic observations after monitoring without making the offline host live', () => {
const original = handle.prepare('SELECT name,container_state,last_error,rcon_failures,last_players,last_roster FROM servers ORDER BY name').all();
handle.prepare("UPDATE servers SET container_state='unknown',last_error='monitor timeout',rcon_failures=2,last_players=NULL,last_roster=NULL").run();
refreshFixtureData(handle, NOW + 60_000);
expect(handle.prepare('SELECT name,container_state,last_error,rcon_failures,last_players,last_roster FROM servers ORDER BY name').all()).toEqual(original);
const hosts = handle.prepare('SELECT last_heartbeat FROM ec_hosts').all() as { last_heartbeat: string }[];
expect(hosts.filter(host => NOW + 60_000 - Date.parse(host.last_heartbeat) > 90_000)).toHaveLength(1);
expect(handle.pragma('foreign_key_check')).toEqual([]);
});
});