Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -6,18 +6,40 @@ import { vi } from 'vitest';
|
||||
export async function mockAgentFiles() {
|
||||
const backend = await import('../../src/lib/server/backends/native.js');
|
||||
const { serverDir } = await import('../../src/lib/server/config.js');
|
||||
return vi.spyOn(backend, 'nativeRequest').mockImplementation(async (instance, kind, payload = {}) => {
|
||||
const path = join(serverDir(instance.server), String(payload.path ?? '.'));
|
||||
let result: unknown;
|
||||
if (kind === 'files.read') {
|
||||
const content = await fs.readFile(path).catch((error: NodeJS.ErrnoException) => { if (error.code === 'ENOENT') return null; throw error; });
|
||||
result = { contentBase64: content?.toString('base64') ?? null };
|
||||
} else if (kind === 'files.stat') {
|
||||
const stat = await fs.stat(path).catch((error: NodeJS.ErrnoException) => { if (error.code === 'ENOENT') return null; throw error; });
|
||||
result = stat ? { exists: true, size: stat.size, mtime: stat.mtime.toISOString(), isDirectory: stat.isDirectory(), isFile: stat.isFile() } : null;
|
||||
} else if (kind === 'files.list') {
|
||||
result = { entries: (await fs.readdir(path, { withFileTypes: true })).map(entry => ({ name: entry.name, isDirectory: entry.isDirectory(), isFile: entry.isFile() })) };
|
||||
} else throw new Error(`Unexpected agent command: ${kind}`);
|
||||
return result as never;
|
||||
});
|
||||
return vi
|
||||
.spyOn(backend, 'nativeRequest')
|
||||
.mockImplementation(async (instance, kind, payload = {}) => {
|
||||
const path = join(serverDir(instance.server), String(payload.path ?? '.'));
|
||||
let result: unknown;
|
||||
if (kind === 'files.read') {
|
||||
const content = await fs.readFile(path).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
result = { contentBase64: content?.toString('base64') ?? null };
|
||||
} else if (kind === 'files.stat') {
|
||||
const stat = await fs.stat(path).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
result = stat
|
||||
? {
|
||||
exists: true,
|
||||
size: stat.size,
|
||||
mtime: stat.mtime.toISOString(),
|
||||
isDirectory: stat.isDirectory(),
|
||||
isFile: stat.isFile()
|
||||
}
|
||||
: null;
|
||||
} else if (kind === 'files.list') {
|
||||
result = {
|
||||
entries: (await fs.readdir(path, { withFileTypes: true })).map((entry) => ({
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
isFile: entry.isFile()
|
||||
}))
|
||||
};
|
||||
} else throw new Error(`Unexpected agent command: ${kind}`);
|
||||
return result as never;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,8 +27,12 @@ export async function newFixture(servers: string[] = ['t5_zombie_theater_1']): P
|
||||
await mkdir(betaArchive, { recursive: true });
|
||||
|
||||
for (const s of servers) {
|
||||
await mkdir(s.startsWith('t6_') ? join(t6, s, 'runtime/plutonium/storage/t6/scripts/zm')
|
||||
: join(t5, s, 'runtime/plutonium/storage/t5/raw/scripts/sp'), { recursive: true });
|
||||
await mkdir(
|
||||
s.startsWith('t6_')
|
||||
? join(t6, s, 'runtime/plutonium/storage/t6/scripts/zm')
|
||||
: join(t5, s, 'runtime/plutonium/storage/t5/raw/scripts/sp'),
|
||||
{ recursive: true }
|
||||
);
|
||||
}
|
||||
|
||||
process.env.SERVERS_ROOT = t5;
|
||||
@@ -49,7 +53,8 @@ export async function newFixture(servers: string[] = ['t5_zombie_theater_1']): P
|
||||
try {
|
||||
const { closeReports } = await import('../../src/lib/server/reports/index.js');
|
||||
await closeReports();
|
||||
const { closeLaunchPreparation } = await import('../../src/lib/server/provisioning/launch-preparation.js');
|
||||
const { closeLaunchPreparation } =
|
||||
await import('../../src/lib/server/provisioning/launch-preparation.js');
|
||||
await closeLaunchPreparation();
|
||||
const mod = (await import('../../src/lib/server/db/index.js')) as {
|
||||
closeDb: () => void;
|
||||
|
||||
@@ -7,27 +7,60 @@ export async function prepareNativeHost(servers: string[] = []) {
|
||||
const libraries = await import('../../src/lib/server/game-files/index.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const host = hosts.createHost({ name: 'Fixture host', portMin: 28960, portMax: 29060 });
|
||||
const inventory = { protocolVersion: 1, cpuCount: 64, cpuUsagePercent: 0,
|
||||
memoryTotal: 128 * 1024 ** 3, memoryAvailable: 120 * 1024 ** 3,
|
||||
dockerRunning: true, directoriesWritable: true, storageAvailable: 100_000_000_000,
|
||||
runtimeImages: [config.defaultRuntimeImage] };
|
||||
const inventory = {
|
||||
protocolVersion: 1,
|
||||
cpuCount: 64,
|
||||
cpuUsagePercent: 0,
|
||||
memoryTotal: 128 * 1024 ** 3,
|
||||
memoryAvailable: 120 * 1024 ** 3,
|
||||
dockerRunning: true,
|
||||
directoriesWritable: true,
|
||||
storageAvailable: 100_000_000_000,
|
||||
runtimeImages: [config.defaultRuntimeImage]
|
||||
};
|
||||
hosts.enrollHost(hosts.createEnrollment(host.id).token, inventory);
|
||||
const versions = new Map<string, string>();
|
||||
for (const game of ['t5', 't6'] as const) {
|
||||
const version = libraries.createLibraryImport({ hostId: host.id, game });
|
||||
const manifest = JSON.stringify([{ path: 'zone/base.ff', size: 4, sha256: createHash('sha256').update('game').digest('hex') }]);
|
||||
const manifest = JSON.stringify([
|
||||
{
|
||||
path: 'zone/base.ff',
|
||||
size: 4,
|
||||
sha256: createHash('sha256').update('game').digest('hex')
|
||||
}
|
||||
]);
|
||||
const hash = createHash('sha256').update(manifest).digest('hex');
|
||||
db().prepare("UPDATE game_file_versions SET state='ready',manifest=?,manifest_sha256=?,size_bytes=4,published_at=? WHERE id=?")
|
||||
db()
|
||||
.prepare(
|
||||
"UPDATE game_file_versions SET state='ready',manifest=?,manifest_sha256=?,size_bytes=4,published_at=? WHERE id=?"
|
||||
)
|
||||
.run(manifest, hash, new Date().toISOString(), version.id);
|
||||
libraries.updateHostLibraryCaches(host.id, [{ libraryVersionId: version.id, status: 'ready', manifestSha256: hash }]);
|
||||
libraries.updateHostLibraryCaches(host.id, [
|
||||
{ libraryVersionId: version.id, status: 'ready', manifestSha256: hash }
|
||||
]);
|
||||
versions.set(game, version.id);
|
||||
}
|
||||
const heartbeat = () => hosts.heartbeat(host.id, { ...inventory, libraries: [...versions.values()].map(id => ({ id, state: 'ready' as const })) });
|
||||
const heartbeat = () =>
|
||||
hosts.heartbeat(host.id, {
|
||||
...inventory,
|
||||
libraries: [...versions.values()].map((id) => ({ id, state: 'ready' as const }))
|
||||
});
|
||||
heartbeat();
|
||||
for (const [index, server] of servers.entries()) {
|
||||
db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES(?,?,?,?,?,?,?)`).run(randomUUID(), server, host.id, versions.get(server.startsWith('t6_') ? 't6' : 't5'),
|
||||
config.defaultRuntimeImage, 28960 + index, new Date().toISOString());
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES(?,?,?,?,?,?,?)`
|
||||
)
|
||||
.run(
|
||||
randomUUID(),
|
||||
server,
|
||||
host.id,
|
||||
versions.get(server.startsWith('t6_') ? 't6' : 't5'),
|
||||
config.defaultRuntimeImage,
|
||||
28960 + index,
|
||||
new Date().toISOString()
|
||||
);
|
||||
}
|
||||
return { host, versions, heartbeat };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
// Integration ticks reflect the production worker: callers submit/observe;
|
||||
// Provisioning alone executes jobs between those reconciliation passes.
|
||||
let campaignFlight: Promise<Awaited<ReturnType<typeof import('../../src/lib/server/pipeline/sweep.js').sweep>>> | null = null;
|
||||
let campaignFlight: Promise<
|
||||
Awaited<ReturnType<typeof import('../../src/lib/server/pipeline/sweep.js').sweep>>
|
||||
> | null = null;
|
||||
export function sweep() {
|
||||
if (campaignFlight) return campaignFlight;
|
||||
campaignFlight = (async () => {
|
||||
const caller = await import('../../src/lib/server/pipeline/sweep.js');
|
||||
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
|
||||
await caller.sweep(); await processProvisioning(); return caller.sweep();
|
||||
})().finally(() => { campaignFlight = null; });
|
||||
await caller.sweep();
|
||||
await processProvisioning();
|
||||
return caller.sweep();
|
||||
})().finally(() => {
|
||||
campaignFlight = null;
|
||||
});
|
||||
return campaignFlight;
|
||||
}
|
||||
let betaFlight: Promise<{ processed: number }> | null = null;
|
||||
@@ -16,8 +22,12 @@ export function betaTick() {
|
||||
betaFlight = (async () => {
|
||||
const caller = await import('../../src/lib/server/pipeline/beta-runner.js');
|
||||
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
|
||||
await caller.betaTick(); await processProvisioning(); return caller.betaTick();
|
||||
})().finally(() => { betaFlight = null; });
|
||||
await caller.betaTick();
|
||||
await processProvisioning();
|
||||
return caller.betaTick();
|
||||
})().finally(() => {
|
||||
betaFlight = null;
|
||||
});
|
||||
return betaFlight;
|
||||
}
|
||||
let maintenanceFlight: Promise<void> | null = null;
|
||||
@@ -26,7 +36,11 @@ export function maintenanceTick(): Promise<void> {
|
||||
maintenanceFlight = (async () => {
|
||||
const caller = await import('../../src/lib/server/fleet/plutonium.js');
|
||||
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
|
||||
await caller.maintenanceTick(); await processProvisioning(); await caller.maintenanceTick();
|
||||
})().finally(() => { maintenanceFlight = null; });
|
||||
await caller.maintenanceTick();
|
||||
await processProvisioning();
|
||||
await caller.maintenanceTick();
|
||||
})().finally(() => {
|
||||
maintenanceFlight = null;
|
||||
});
|
||||
return maintenanceFlight;
|
||||
}
|
||||
|
||||
@@ -4,5 +4,7 @@ export function initializeStandaloneSettings(handle: Database.Database) {
|
||||
handle.exec(`CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY,value TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS retention_controls (category TEXT NOT NULL,record_id TEXT NOT NULL,protected INTEGER NOT NULL DEFAULT 0,resolved_at TEXT,PRIMARY KEY(category,record_id));
|
||||
CREATE TABLE IF NOT EXISTS retention_expirations (category TEXT NOT NULL,record_id TEXT NOT NULL,expired_at TEXT NOT NULL,PRIMARY KEY(category,record_id));`);
|
||||
handle.prepare("INSERT OR IGNORE INTO settings(key,value) VALUES('fleet_settings',?)").run(JSON.stringify(defaultFleetSettings));
|
||||
handle
|
||||
.prepare("INSERT OR IGNORE INTO settings(key,value) VALUES('fleet_settings',?)")
|
||||
.run(JSON.stringify(defaultFleetSettings));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/** File/history unit tests simulate the activation leaf. Provisioning tests cover
|
||||
* the occupancy, capacity, source, and restart gates before reaching this leaf. */
|
||||
export async function activateFiles(kind: 'beta' | 'campaign', id: number | string): Promise<void> {
|
||||
const { stagedFileChanges, applyStagedFilesForOperation } = await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const { stagedFileChanges, applyStagedFilesForOperation } =
|
||||
await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const { requestRestart } = await import('../../src/lib/server/provisioning/index.js');
|
||||
for (const server of new Set(stagedFileChanges(kind, String(id)).filter(row => row.state === 'pending').map(row => row.server))) {
|
||||
for (const server of new Set(
|
||||
stagedFileChanges(kind, String(id))
|
||||
.filter((row) => row.state === 'pending')
|
||||
.map((row) => row.server)
|
||||
)) {
|
||||
const operation = requestRestart(server, { kind, id: String(id) });
|
||||
applyStagedFilesForOperation(operation.id, server);
|
||||
}
|
||||
|
||||
@@ -6,18 +6,34 @@ export function telemetryAuthHeaders(): Record<string, string> {
|
||||
return { Authorization: `Bearer ${TEST_TELEMETRY_TOKEN}` };
|
||||
}
|
||||
|
||||
export const roster = (count = 4): TelemetryPlayerStats[] => Array.from({ length: count }, (_, i) => ({
|
||||
name: `Player ${i + 1}`, guid: `1234567890123456${i}`, kills: 100 + i,
|
||||
headshots: 50, downs: 1, revives: 2, score: 5000
|
||||
}));
|
||||
export function gameEvent(event: GameEvent['event'], round = 25, players = 4, server = 't5_zombie_theater_1', map = 'zombie_theater'): GameEvent {
|
||||
export const roster = (count = 4): TelemetryPlayerStats[] =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
name: `Player ${i + 1}`,
|
||||
guid: `1234567890123456${i}`,
|
||||
kills: 100 + i,
|
||||
headshots: 50,
|
||||
downs: 1,
|
||||
revives: 2,
|
||||
score: 5000
|
||||
}));
|
||||
export function gameEvent(
|
||||
event: GameEvent['event'],
|
||||
round = 25,
|
||||
players = 4,
|
||||
server = 't5_zombie_theater_1',
|
||||
map = 'zombie_theater'
|
||||
): GameEvent {
|
||||
return event === 'game_start'
|
||||
? { event, server, map, players: roster(players).map(({ name, guid }) => ({ name, guid })) }
|
||||
: { event, server, map, round, players: roster(players) };
|
||||
}
|
||||
export async function postTelemetry(body: unknown, contentType = 'application/json') {
|
||||
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
|
||||
return POST({ request: new Request('http://localhost/api/telemetry', {
|
||||
method: 'POST', headers: { 'Content-Type': contentType, ...telemetryAuthHeaders() }, body: JSON.stringify(body)
|
||||
}) });
|
||||
return POST({
|
||||
request: new Request('http://localhost/api/telemetry', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': contentType, ...telemetryAuthHeaders() },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ try {
|
||||
handle.pragma('cache_size = -16384');
|
||||
handle.exec('CREATE TABLE records(id INTEGER PRIMARY KEY, content BLOB)');
|
||||
const insert = handle.prepare('INSERT INTO records(content) VALUES(zeroblob(1048576))');
|
||||
handle.transaction(() => { for (let i = 0; i < 256; i++) insert.run(); })();
|
||||
handle.transaction(() => {
|
||||
for (let i = 0; i < 256; i++) insert.run();
|
||||
})();
|
||||
handle.pragma('wal_checkpoint(TRUNCATE)');
|
||||
handle.pragma('shrink_memory');
|
||||
// Reproduce the dangerous caller settings. The rebuild must override these.
|
||||
@@ -16,8 +18,15 @@ try {
|
||||
handle.pragma('cache_size = -131072');
|
||||
const beforeKiB = process.resourceUsage().maxRSS;
|
||||
enableIncrementalVacuum(handle);
|
||||
console.log(JSON.stringify({ beforeKiB, peakKiB: process.resourceUsage().maxRSS,
|
||||
vacuum: handle.pragma('auto_vacuum', { simple: true }),
|
||||
integrity: handle.pragma('quick_check', { simple: true }),
|
||||
rows: handle.prepare('SELECT COUNT(*) n FROM records').get() }));
|
||||
} finally { handle.close(); }
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
beforeKiB,
|
||||
peakKiB: process.resourceUsage().maxRSS,
|
||||
vacuum: handle.pragma('auto_vacuum', { simple: true }),
|
||||
integrity: handle.pragma('quick_check', { simple: true }),
|
||||
rows: handle.prepare('SELECT COUNT(*) n FROM records').get()
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
handle.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user