Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+265
-65
@@ -1,12 +1,26 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { access, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
||||
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 {
|
||||
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');
|
||||
@@ -14,24 +28,42 @@ 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 }); });
|
||||
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/);
|
||||
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/);
|
||||
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 () => {
|
||||
@@ -49,7 +81,9 @@ describe('fixture destination isolation', () => {
|
||||
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/);
|
||||
await expect(reserveFixture(join(root, 'preview', 'database.db'))).rejects.toThrow(
|
||||
/already exists/
|
||||
);
|
||||
expect(await readFile(target.dbPath, 'utf8')).toBe('do not replace');
|
||||
});
|
||||
|
||||
@@ -57,12 +91,32 @@ describe('fixture destination isolation', () => {
|
||||
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' }
|
||||
});
|
||||
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();
|
||||
@@ -78,19 +132,50 @@ describe('fixture destination isolation', () => {
|
||||
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(); }
|
||||
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');
|
||||
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(
|
||||
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);
|
||||
@@ -100,17 +185,23 @@ describe('useful synthetic fixture data', () => {
|
||||
let fixture: Fixture;
|
||||
let handle: Database.Database;
|
||||
beforeAll(async () => {
|
||||
vi.resetModules(); fixture = await newFixture([]);
|
||||
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);
|
||||
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(); });
|
||||
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', () => {
|
||||
@@ -119,78 +210,187 @@ describe('useful synthetic fixture data', () => {
|
||||
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(
|
||||
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);
|
||||
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 });
|
||||
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);
|
||||
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 });
|
||||
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(
|
||||
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();
|
||||
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');
|
||||
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);
|
||||
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();
|
||||
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);
|
||||
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();
|
||||
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
|
||||
.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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user