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([]); }); });