Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+395 -181
View File
@@ -10,10 +10,13 @@ let fixture: Fixture;
async function applySource(kind: 'campaign' | 'beta', id: string) {
const { db } = await import('../../src/lib/server/db/index.js');
const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js');
const { applyStagedFilesForOperation } = await import('../../src/lib/server/fleet/staged-files.js');
const { applyStagedFilesForOperation } =
await import('../../src/lib/server/fleet/staged-files.js');
const table = kind === 'campaign' ? 'deployment_servers' : 'beta_servers';
const key = kind === 'campaign' ? 'deployment_id' : 'beta_id';
const servers = db().prepare(`SELECT server FROM ${table} WHERE ${key} = ?`).all(id) as { server: string }[];
const servers = db().prepare(`SELECT server FROM ${table} WHERE ${key} = ?`).all(id) as {
server: string;
}[];
for (const { server } of servers) {
const operation = requestRestart(server, { kind, id });
applyStagedFilesForOperation(operation.id, server);
@@ -22,7 +25,9 @@ async function applySource(kind: 'campaign' | 'beta', id: string) {
// Scanner tests exercise the queued file application separately from the
// Provisioning occupancy/concurrency integration tests.
async function applyDriftDeployment(input: import('../../src/lib/server/fleet/drift.js').RunDriftInput = {}) {
async function applyDriftDeployment(
input: import('../../src/lib/server/fleet/drift.js').RunDriftInput = {}
) {
const { runDriftDeployment } = await import('../../src/lib/server/fleet/drift.js');
const result = await runDriftDeployment(input);
if (result.deploymentId) await applySource('campaign', result.deploymentId);
@@ -35,9 +40,14 @@ beforeEach(async () => {
delete process.env.RCON_PASSWORD;
// Ensure the three per-kind destinations exist under the fixture tree.
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/storage/t5/raw/scripts/sp'), { recursive: true });
await mkdir(
join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/storage/t5/raw/scripts/sp'),
{ recursive: true }
);
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'configs'), { recursive: true });
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/plugins'), { recursive: true });
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/plugins'), {
recursive: true
});
});
afterEach(async () => {
@@ -50,18 +60,28 @@ describe('reviewed drift plans', () => {
await seedServerRegistry();
const { destOfKind } = await import('../../src/lib/server/config.js');
const destination = destOfKind('t5_zombie_theater_1', 'plugin');
const names = Array.from({ length: 12 }, (_, i) => `batch-${String(i).padStart(2, '0')}.dll`);
await Promise.all(names.map(name => writeFile(join(destination, name), name)));
const names = Array.from(
{ length: 12 },
(_, i) => `batch-${String(i).padStart(2, '0')}.dll`
);
await Promise.all(names.map((name) => writeFile(join(destination, name), name)));
const files = await import('../../src/lib/server/backends/files.js');
const original = files.readFile;
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
let active = 0; let peak = 0;
const read = vi.spyOn(files, 'readFile').mockImplementation(async path => {
const gate = new Promise<void>((resolve) => {
release = resolve;
});
let active = 0;
let peak = 0;
const read = vi.spyOn(files, 'readFile').mockImplementation(async (path) => {
if (!path.includes('batch-')) return original(path);
peak = Math.max(peak, ++active);
try { await gate; return await original(path); }
finally { active--; }
try {
await gate;
return await original(path);
} finally {
active--;
}
});
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const scan = scanServerDrift('t5_zombie_theater_1');
@@ -70,10 +90,18 @@ describe('reviewed drift plans', () => {
expect(read.mock.calls.filter(([path]) => path.includes('batch-'))).toHaveLength(4);
release();
const result = await scan;
expect(result.filter(entry => entry.relPath.startsWith('batch-')).map(entry => entry.relPath)).toEqual(names);
expect(
result
.filter((entry) => entry.relPath.startsWith('batch-'))
.map((entry) => entry.relPath)
).toEqual(names);
expect(peak).toBe(4);
expect(active).toBe(0);
} finally { release(); await scan; read.mockRestore(); }
} finally {
release();
await scan;
read.mockRestore();
}
});
it('keeps live deletions, replacements, and inventory unchanged until activation', async () => {
@@ -85,9 +113,17 @@ describe('reviewed drift plans', () => {
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const { script, version } = createScript({ name: 'managed plugin', kind: 'plugin', scopeType: 'game', scopeValue: 't5',
targetRelPath: 'managed.dll', content: Buffer.from('new plugin') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const { script, version } = createScript({
name: 'managed plugin',
kind: 'plugin',
scopeType: 'game',
scopeValue: 't5',
targetRelPath: 'managed.dll',
content: Buffer.from('new plugin')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
const target = join(destOfKind(server, 'plugin'), 'managed.dll');
const stray = join(destOfKind(server, 'config'), 'stray.cfg');
await writeFile(target, 'current plugin');
@@ -105,12 +141,16 @@ describe('reviewed drift plans', () => {
it('preserves a file changed while its reviewed deletion waits for activation', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { previewDrift, runDriftDeployment } = await import('../../src/lib/server/fleet/drift.js');
const { previewDrift, runDriftDeployment } =
await import('../../src/lib/server/fleet/drift.js');
await seedServerRegistry();
const path = join(fixture.t5, 't5_zombie_theater_1', 'configs', 'stray.cfg');
await writeFile(path, 'reviewed');
const plan = await previewDrift(['t5_zombie_theater_1']);
const result = await runDriftDeployment({ servers: plan.servers, expectedFingerprint: plan.fingerprint });
const result = await runDriftDeployment({
servers: plan.servers,
expectedFingerprint: plan.fingerprint
});
await writeFile(path, 'new operator settings');
await expect(applySource('campaign', result.deploymentId!)).rejects.toThrow();
expect(await readFile(path, 'utf8')).toBe('new operator settings');
@@ -118,40 +158,64 @@ describe('reviewed drift plans', () => {
it('rejects changed file bytes before any deletion and returns a fresh review', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { previewDrift, DriftPlanChanged } = await import('../../src/lib/server/fleet/drift.js');
const { previewDrift, DriftPlanChanged } =
await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
await seedServerRegistry();
const path = join(fixture.t5, 't5_zombie_theater_1', 'configs', 'stray.cfg');
await writeFile(path, 'original');
const plan = await previewDrift(['t5_zombie_theater_1']);
await writeFile(path, 'changed');
await expect(applyDriftDeployment({ servers: plan.servers, expectedFingerprint: plan.fingerprint })).rejects.toBeInstanceOf(DriftPlanChanged);
await expect(
applyDriftDeployment({ servers: plan.servers, expectedFingerprint: plan.fingerprint })
).rejects.toBeInstanceOf(DriftPlanChanged);
expect(await readFile(path, 'utf8')).toBe('changed');
expect(db().prepare('SELECT * FROM deployments').all()).toEqual([]);
const refreshed = await previewDrift(plan.servers);
expect(refreshed.fingerprint).not.toBe(plan.fingerprint);
expect((await applyDriftDeployment({ servers: refreshed.servers, expectedFingerprint: refreshed.fingerprint })).totalRemoved).toBe(1);
expect(
(
await applyDriftDeployment({
servers: refreshed.servers,
expectedFingerprint: refreshed.fingerprint
})
).totalRemoved
).toBe(1);
});
it('rejects changed replacement bytes and preserves the reviewed existing file', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { previewDrift, DriftPlanChanged } = await import('../../src/lib/server/fleet/drift.js');
const { previewDrift, DriftPlanChanged } =
await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
await seedServerRegistry();
const { script, version } = createScript({ name: 'Managed config', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\n') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const { script, version } = createScript({
name: 'Managed config',
kind: 'baseline',
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('set sv_voice "1"\n')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
const path = join(fixture.t5, 't5_zombie_theater_1', 'configs', 'dedicated_sp.cfg');
await writeFile(path, 'existing settings');
const plan = await previewDrift(['t5_zombie_theater_1']);
db().prepare('UPDATE script_versions SET content_hash = ? WHERE id = ?').run(putFileBlob(Buffer.from('set sv_voice "0"\n'), db()), version.id);
await expect(applyDriftDeployment({ servers: plan.servers, expectedFingerprint: plan.fingerprint })).rejects.toBeInstanceOf(DriftPlanChanged);
db()
.prepare('UPDATE script_versions SET content_hash = ? WHERE id = ?')
.run(putFileBlob(Buffer.from('set sv_voice "0"\n'), db()), version.id);
await expect(
applyDriftDeployment({ servers: plan.servers, expectedFingerprint: plan.fingerprint })
).rejects.toBeInstanceOf(DriftPlanChanged);
expect(await readFile(path, 'utf8')).toBe('existing settings');
});
it('keeps selected reconciliation within its reviewed server identities', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { previewDrift, DriftPlanChanged } = await import('../../src/lib/server/fleet/drift.js');
const { previewDrift, DriftPlanChanged } =
await import('../../src/lib/server/fleet/drift.js');
const second = join(fixture.t5, 't5_zombie_theater_2', 'configs');
await mkdir(second, { recursive: true });
await seedServerRegistry();
@@ -159,8 +223,13 @@ describe('reviewed drift plans', () => {
await writeFile(firstPath, 'one');
await writeFile(join(second, 'stray.cfg'), 'two');
const plan = await previewDrift(['t5_zombie_theater_1']);
await expect(applyDriftDeployment({ expectedFingerprint: plan.fingerprint })).rejects.toBeInstanceOf(DriftPlanChanged);
await applyDriftDeployment({ servers: plan.servers, expectedFingerprint: plan.fingerprint });
await expect(
applyDriftDeployment({ expectedFingerprint: plan.fingerprint })
).rejects.toBeInstanceOf(DriftPlanChanged);
await applyDriftDeployment({
servers: plan.servers,
expectedFingerprint: plan.fingerprint
});
expect(existsSync(firstPath)).toBe(false);
expect(await readFile(join(second, 'stray.cfg'), 'utf8')).toBe('two');
});
@@ -168,95 +237,165 @@ describe('reviewed drift plans', () => {
describe('drift scanning', () => {
it.each([
['script', 'moved'], ['script', 'unset'], ['plugin', 'moved'], ['plugin', 'unset']
] as const)('detects formerly tracked %s files after their scope is %s', async (kind, change) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript, setScope } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const relPath = kind === 'script' ? 'outdated.gsc' : 'outdated.dll';
const { script, version } = createScript({ name: `outdated ${kind}`, kind, scopeType: 'game', scopeValue: 't5', targetRelPath: relPath, content: Buffer.from('previously promoted') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
await applyDriftDeployment({ servers: [server] });
setScope({ scriptId: script.id, scopeType: change === 'moved' ? 'game' : null, scopeValue: change === 'moved' ? 't6' : null });
expect(getServerScript(server, relPath)).toBeDefined();
expect(await scanServerDrift(server)).toEqual([expect.objectContaining({ kind, relPath, driftKind: 'untracked' })]);
await applyDriftDeployment({ servers: [server] });
expect(existsSync(join(destOfKind(server, kind), relPath))).toBe(false);
expect(getServerScript(server, relPath)).toBeUndefined();
});
['script', 'moved'],
['script', 'unset'],
['plugin', 'moved'],
['plugin', 'unset']
] as const)(
'detects formerly tracked %s files after their scope is %s',
async (kind, change) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript, setScope } =
await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const relPath = kind === 'script' ? 'outdated.gsc' : 'outdated.dll';
const { script, version } = createScript({
name: `outdated ${kind}`,
kind,
scopeType: 'game',
scopeValue: 't5',
targetRelPath: relPath,
content: Buffer.from('previously promoted')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
await applyDriftDeployment({ servers: [server] });
setScope({
scriptId: script.id,
scopeType: change === 'moved' ? 'game' : null,
scopeValue: change === 'moved' ? 't6' : null
});
expect(getServerScript(server, relPath)).toBeDefined();
expect(await scanServerDrift(server)).toEqual([
expect.objectContaining({ kind, relPath, driftKind: 'untracked' })
]);
await applyDriftDeployment({ servers: [server] });
expect(existsSync(join(destOfKind(server, kind), relPath))).toBe(false);
expect(getServerScript(server, relPath)).toBeUndefined();
}
);
it.each(['script', 'plugin'] as const)('detects edited tracked %s bytes and restores their promoted version', async (kind) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const relPath = kind === 'script' ? 'managed.gsc' : 'managed.dll';
const content = Buffer.from('promoted content');
const { script, version } = createScript({ name: `managed ${kind}`, kind, scopeType: 'game', scopeValue: 't5', targetRelPath: relPath, content });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
await applyDriftDeployment({ servers: [server] });
const inventory = getServerScript(server, relPath);
const path = join(destOfKind(server, kind), relPath);
await writeFile(path, 'changed outside Electric Cherry');
expect(getServerScript(server, relPath)).toEqual(inventory);
expect(await scanServerDrift(server)).toEqual([expect.objectContaining({ relPath, kind, driftKind: 'replace' })]);
await applyDriftDeployment({ servers: [server] });
expect(await readFile(path)).toEqual(content);
expect(await scanServerDrift(server)).toEqual([]);
});
it.each(['script', 'plugin'] as const)(
'detects edited tracked %s bytes and restores their promoted version',
async (kind) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const relPath = kind === 'script' ? 'managed.gsc' : 'managed.dll';
const content = Buffer.from('promoted content');
const { script, version } = createScript({
name: `managed ${kind}`,
kind,
scopeType: 'game',
scopeValue: 't5',
targetRelPath: relPath,
content
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
await applyDriftDeployment({ servers: [server] });
const inventory = getServerScript(server, relPath);
const path = join(destOfKind(server, kind), relPath);
await writeFile(path, 'changed outside Electric Cherry');
expect(getServerScript(server, relPath)).toEqual(inventory);
expect(await scanServerDrift(server)).toEqual([
expect.objectContaining({ relPath, kind, driftKind: 'replace' })
]);
await applyDriftDeployment({ servers: [server] });
expect(await readFile(path)).toEqual(content);
expect(await scanServerDrift(server)).toEqual([]);
}
);
it('compares composed config with disk even when inventory still has the promoted hash', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { getPromotedServerConfig } = await import('../../src/lib/server/fleet/server-config.js');
const { getPromotedServerConfig } =
await import('../../src/lib/server/fleet/server-config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const { script, version } = createScript({ name: 'baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\n') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const { script, version } = createScript({
name: 'baseline',
kind: 'baseline',
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('set sv_voice "1"\n')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
await applyDriftDeployment({ servers: [server] });
const path = join(fixture.t5, server, 'configs', 'dedicated_sp.cfg');
await writeFile(path, 'changed outside Electric Cherry');
expect(await scanServerDrift(server)).toEqual([expect.objectContaining({ relPath: 'dedicated_sp.cfg', driftKind: 'replace' })]);
expect(await scanServerDrift(server)).toEqual([
expect.objectContaining({ relPath: 'dedicated_sp.cfg', driftKind: 'replace' })
]);
await applyDriftDeployment({ servers: [server] });
expect(await readFile(path)).toEqual(getPromotedServerConfig('t5', 'zombie_theater'));
});
it.each(['running', 'ready-promote'])('defers all drift for a server with a %s beta', async (state) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript, uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const { script, version } = createScript({ name: 'baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\n') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const experimental = uploadNewVersion(script.id, Buffer.from('set sv_voice "0"\n'));
const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: experimental.id }], servers: [server], waitForHealth: false, waitForMatch: false });
await applySource('beta', String(betaId));
db().prepare('UPDATE betas SET state = ? WHERE id = ?').run(state, betaId);
const path = join(fixture.t5, server, 'configs', 'dedicated_sp.cfg');
const betaBytes = await readFile(path);
const stray = join(fixture.t5, server, 'configs', 'stray.cfg');
await writeFile(stray, 'also deferred');
expect(await scanServerDrift(server)).toEqual([]);
expect(await applyDriftDeployment({ servers: [server] })).toMatchObject({ deploymentId: null, totalServers: 0 });
expect(await readFile(path)).toEqual(betaBytes);
expect(await readFile(stray, 'utf8')).toBe('also deferred');
db().prepare("UPDATE betas SET state = 'rolled-back' WHERE id = ?").run(betaId);
expect((await scanServerDrift(server)).map(entry => entry.relPath)).toEqual(['dedicated_sp.cfg', 'stray.cfg']);
});
it.each(['running', 'ready-promote'])(
'defers all drift for a server with a %s beta',
async (state) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript, uploadNewVersion } =
await import('../../src/lib/server/fleet/scripts.js');
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const { script, version } = createScript({
name: 'baseline',
kind: 'baseline',
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('set sv_voice "1"\n')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
const experimental = uploadNewVersion(script.id, Buffer.from('set sv_voice "0"\n'));
const { betaId } = await openBeta({
scripts: [{ scriptId: script.id, scriptVersionId: experimental.id }],
servers: [server],
waitForHealth: false,
waitForMatch: false
});
await applySource('beta', String(betaId));
db().prepare('UPDATE betas SET state = ? WHERE id = ?').run(state, betaId);
const path = join(fixture.t5, server, 'configs', 'dedicated_sp.cfg');
const betaBytes = await readFile(path);
const stray = join(fixture.t5, server, 'configs', 'stray.cfg');
await writeFile(stray, 'also deferred');
expect(await scanServerDrift(server)).toEqual([]);
expect(await applyDriftDeployment({ servers: [server] })).toMatchObject({
deploymentId: null,
totalServers: 0
});
expect(await readFile(path)).toEqual(betaBytes);
expect(await readFile(stray, 'utf8')).toBe('also deferred');
db().prepare("UPDATE betas SET state = 'rolled-back' WHERE id = ?").run(betaId);
expect((await scanServerDrift(server)).map((entry) => entry.relPath)).toEqual([
'dedicated_sp.cfg',
'stray.cfg'
]);
}
);
it('reports an unavailable registered directory without hiding drift on another game root', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
@@ -267,11 +406,17 @@ describe('drift scanning', () => {
await writeFile(join(fixture.t6, other, 'configs', 'stray.cfg'), 'untracked');
await seedServerRegistry();
await rm(fixture.t5, { recursive: true });
await expect(scanServerDrift('t5_zombie_theater_1')).rejects.toThrow('Server files unavailable');
await expect(applyDriftDeployment({ servers: ['t5_zombie_theater_1'] })).rejects.toThrow('Server files unavailable');
await expect(scanServerDrift('t5_zombie_theater_1')).rejects.toThrow(
'Server files unavailable'
);
await expect(applyDriftDeployment({ servers: ['t5_zombie_theater_1'] })).rejects.toThrow(
'Server files unavailable'
);
expect(existsSync(fixture.t5)).toBe(false);
expect(db().prepare('SELECT * FROM deployments').all()).toEqual([]);
expect(await scanServerDrift(other)).toEqual([expect.objectContaining({ server: other, relPath: 'stray.cfg', driftKind: 'untracked' })]);
expect(await scanServerDrift(other)).toEqual([
expect.objectContaining({ server: other, relPath: 'stray.cfg', driftKind: 'untracked' })
]);
});
it('replaces a seeded config once instead of planning both removal and installation', async () => {
@@ -279,49 +424,84 @@ describe('drift scanning', () => {
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { getPromotedServerConfig } = await import('../../src/lib/server/fleet/server-config.js');
const { getPromotedServerConfig } =
await import('../../src/lib/server/fleet/server-config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const { script, version } = createScript({ name: 'baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5',
content: Buffer.from('set sv_voice "1"\n') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const { script, version } = createScript({
name: 'baseline',
kind: 'baseline',
scopeType: 'game',
scopeValue: 't5',
content: Buffer.from('set sv_voice "1"\n')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
const path = join(fixture.t5, server, 'configs', 'dedicated_sp.cfg');
await writeFile(path, 'seeded config');
expect(await scanServerDrift(server)).toEqual([expect.objectContaining({ kind: 'config', driftKind: 'replace' })]);
expect(await applyDriftDeployment({ servers: [server] })).toMatchObject({ totalRemoved: 0, totalRestored: 1 });
expect(await scanServerDrift(server)).toEqual([
expect.objectContaining({ kind: 'config', driftKind: 'replace' })
]);
expect(await applyDriftDeployment({ servers: [server] })).toMatchObject({
totalRemoved: 0,
totalRestored: 1
});
expect(await readFile(path)).toEqual(getPromotedServerConfig('t5', 'zombie_theater'));
expect(await scanServerDrift(server)).toEqual([]);
});
it.each(['identical', 'different'])('reconciles an existing %s scoped file in one pass with an update audit', async (mode) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const content = Buffer.from('main(){}\n');
const before = mode === 'identical' ? content : Buffer.from('old bytes');
const { script, version } = createScript({ name: 'spawn fix', kind: 'script', scopeType: 'game', scopeValue: 't5',
targetRelPath: 'zm_spawn_fix.gsc', content });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const target = join(destOfKind(server, 'script'), 'zm_spawn_fix.gsc');
await writeFile(target, before);
expect(await scanServerDrift(server)).toEqual([expect.objectContaining({ relPath: 'zm_spawn_fix.gsc', driftKind: 'replace' })]);
expect(await applyDriftDeployment({ servers: [server], dryRun: true })).toMatchObject({ totalRemoved: 0, totalRestored: 1, deploymentId: null });
expect(await readFile(target)).toEqual(before);
expect(getServerScript(server, 'zm_spawn_fix.gsc')).toBeUndefined();
const result = await applyDriftDeployment({ servers: [server] });
expect(result).toMatchObject({ totalRemoved: 0, totalRestored: 1 });
expect(await readFile(target)).toEqual(content);
expect(getServerScript(server, 'zm_spawn_fix.gsc')?.script_version_id).toBe(version.id);
expect(db().prepare('SELECT verb, (SELECT content FROM file_blobs WHERE sha256=content_before_hash) AS content_before, (SELECT content FROM file_blobs WHERE sha256=content_after_hash) AS content_after FROM deployment_files WHERE deployment_id = ?').all(result.deploymentId))
.toEqual([{ verb: 'updated', content_before: before, content_after: content }]);
expect(await scanServerDrift(server)).toEqual([]);
expect((await applyDriftDeployment({ servers: [server] })).deploymentId).toBeNull();
});
it.each(['identical', 'different'])(
'reconciles an existing %s scoped file in one pass with an update audit',
async (mode) => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const content = Buffer.from('main(){}\n');
const before = mode === 'identical' ? content : Buffer.from('old bytes');
const { script, version } = createScript({
name: 'spawn fix',
kind: 'script',
scopeType: 'game',
scopeValue: 't5',
targetRelPath: 'zm_spawn_fix.gsc',
content
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
const target = join(destOfKind(server, 'script'), 'zm_spawn_fix.gsc');
await writeFile(target, before);
expect(await scanServerDrift(server)).toEqual([
expect.objectContaining({ relPath: 'zm_spawn_fix.gsc', driftKind: 'replace' })
]);
expect(await applyDriftDeployment({ servers: [server], dryRun: true })).toMatchObject({
totalRemoved: 0,
totalRestored: 1,
deploymentId: null
});
expect(await readFile(target)).toEqual(before);
expect(getServerScript(server, 'zm_spawn_fix.gsc')).toBeUndefined();
const result = await applyDriftDeployment({ servers: [server] });
expect(result).toMatchObject({ totalRemoved: 0, totalRestored: 1 });
expect(await readFile(target)).toEqual(content);
expect(getServerScript(server, 'zm_spawn_fix.gsc')?.script_version_id).toBe(version.id);
expect(
db()
.prepare(
'SELECT verb, (SELECT content FROM file_blobs WHERE sha256=content_before_hash) AS content_before, (SELECT content FROM file_blobs WHERE sha256=content_after_hash) AS content_after FROM deployment_files WHERE deployment_id = ?'
)
.all(result.deploymentId)
).toEqual([{ verb: 'updated', content_before: before, content_after: content }]);
expect(await scanServerDrift(server)).toEqual([]);
expect((await applyDriftDeployment({ servers: [server] })).deploymentId).toBeNull();
}
);
it('detects untracked files across script, config, and plugin destinations', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
@@ -334,10 +514,7 @@ describe('drift scanning', () => {
join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc'),
'main(){}\n'
);
await writeFile(
join(fixture.t5, server, 'configs', 'stray.cfg'),
'set foo "bar"\n'
);
await writeFile(join(fixture.t5, server, 'configs', 'stray.cfg'), 'set foo "bar"\n');
await writeFile(
join(fixture.t5, server, 'runtime/plutonium/plugins', 'stray.dll'),
Buffer.from([0xde, 0xad])
@@ -357,40 +534,85 @@ describe('drift rebuild campaigns', () => {
it('captures intended changes without writing and follows the linked rebuild without another restart', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { prepareDriftCampaign, createDriftRebuildCampaign } = await import('../../src/lib/server/fleet/drift.js');
const { prepareDriftCampaign, createDriftRebuildCampaign } =
await import('../../src/lib/server/fleet/drift.js');
const { beginDeployment } = await import('../../src/lib/server/provisioning/history.js');
const { sweep } = await import('../../src/lib/server/pipeline/sweep.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
await seedServerRegistry();
const server = 't5_zombie_theater_1';
const { script, version } = createScript({ name: 'managed', kind: 'script', scopeType: 'game', scopeValue: 't5', targetRelPath: 'managed.gsc', content: Buffer.from('promoted content') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(version.id, script.id);
const { script, version } = createScript({
name: 'managed',
kind: 'script',
scopeType: 'game',
scopeValue: 't5',
targetRelPath: 'managed.gsc',
content: Buffer.from('promoted content')
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(version.id, script.id);
const target = join(destOfKind(server, 'script'), 'managed.gsc');
await writeFile(target, 'old content');
const stray = join(destOfKind(server, 'config'), 'stray.cfg');
await writeFile(stray, 'untracked');
const prepared = await prepareDriftCampaign(server);
const operationId = beginDeployment(server, 'rebuild', 'watchdog');
db().prepare("UPDATE server_deployments SET state = 'waiting' WHERE id = ?").run(operationId);
db()
.prepare("UPDATE server_deployments SET state = 'waiting' WHERE id = ?")
.run(operationId);
const campaignId = createDriftRebuildCampaign(server, operationId, 42, prepared);
expect(campaignId).toBeTruthy();
expect(await readFile(target, 'utf8')).toBe('old content');
expect(await readFile(stray, 'utf8')).toBe('untracked');
expect(db().prepare('SELECT rel_path, verb, (SELECT content FROM file_blobs WHERE sha256=content_before_hash) AS content_before, (SELECT content FROM file_blobs WHERE sha256=content_after_hash) AS content_after FROM deployment_files WHERE deployment_id = ? ORDER BY rel_path').all(campaignId))
.toEqual([
{ rel_path: 'managed.gsc', verb: 'updated', content_before: Buffer.from('old content'), content_after: (await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(version.id) },
{ rel_path: 'stray.cfg', verb: 'removed', content_before: Buffer.from('untracked'), content_after: null }
]);
expect(
db()
.prepare(
'SELECT rel_path, verb, (SELECT content FROM file_blobs WHERE sha256=content_before_hash) AS content_before, (SELECT content FROM file_blobs WHERE sha256=content_after_hash) AS content_after FROM deployment_files WHERE deployment_id = ? ORDER BY rel_path'
)
.all(campaignId)
).toEqual([
{
rel_path: 'managed.gsc',
verb: 'updated',
content_before: Buffer.from('old content'),
content_after: (
await import('../../src/lib/server/fleet/scripts.js')
).getVersionContent(version.id)
},
{
rel_path: 'stray.cfg',
verb: 'removed',
content_before: Buffer.from('untracked'),
content_after: null
}
]);
await sweep();
expect(db().prepare('SELECT action FROM server_deployments').all()).toEqual([{ action: 'rebuild' }]);
expect(db().prepare('SELECT provisioning_id, restart_state FROM deployment_servers WHERE deployment_id = ?').get(campaignId))
.toEqual({ provisioning_id: operationId, restart_state: 'pending' });
db().prepare("UPDATE server_deployments SET state = 'done', submitted_at = ?, finished_at = ? WHERE id = ?")
expect(db().prepare('SELECT action FROM server_deployments').all()).toEqual([
{ action: 'rebuild' }
]);
expect(
db()
.prepare(
'SELECT provisioning_id, restart_state FROM deployment_servers WHERE deployment_id = ?'
)
.get(campaignId)
).toEqual({ provisioning_id: operationId, restart_state: 'pending' });
db()
.prepare(
"UPDATE server_deployments SET state = 'done', submitted_at = ?, finished_at = ? WHERE id = ?"
)
.run(new Date().toISOString(), new Date().toISOString(), operationId);
await sweep();
expect(db().prepare('SELECT restart_state FROM deployment_servers WHERE deployment_id = ?').get(campaignId)).toEqual({ restart_state: 'done' });
expect(db().prepare('SELECT finished_at FROM deployments WHERE id = ?').get(campaignId)).toMatchObject({ finished_at: expect.any(String) });
expect(
db()
.prepare('SELECT restart_state FROM deployment_servers WHERE deployment_id = ?')
.get(campaignId)
).toEqual({ restart_state: 'done' });
expect(
db().prepare('SELECT finished_at FROM deployments WHERE id = ?').get(campaignId)
).toMatchObject({ finished_at: expect.any(String) });
});
});
@@ -431,9 +653,9 @@ async function promoteAndOrphan(
waitForMatch: false
});
await applySource('beta', String(betaId));
db().prepare(
`UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?`
).run(new Date().toISOString(), betaId);
db()
.prepare(`UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?`)
.run(new Date().toISOString(), betaId);
db().prepare(`UPDATE betas SET state = 'ready-promote' WHERE id = ?`).run(betaId);
await promoteBeta(betaId);
return { scriptId: created.script.id };
@@ -442,15 +664,10 @@ async function promoteAndOrphan(
describe('missing artifact detection', () => {
it('flags a promoted script that is absent on a scope-matching server as missing, then restores it on reconcile', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript, getLatestVersion } = await import(
'../../src/lib/server/fleet/scripts.js'
);
const { openBeta, promoteBeta } = await import(
'../../src/lib/server/fleet/beta.js'
);
const { scanServerDrift } = await import(
'../../src/lib/server/fleet/drift.js'
);
const { createScript, getLatestVersion } =
await import('../../src/lib/server/fleet/scripts.js');
const { openBeta, promoteBeta } = await import('../../src/lib/server/fleet/beta.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
await seedServerRegistry();
@@ -492,12 +709,9 @@ describe('missing artifact detection', () => {
it('flags a scope-matching server whose composed dedicated_sp.cfg is missing', async () => {
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
const { createScript, getLatestVersion } = await import(
'../../src/lib/server/fleet/scripts.js'
);
const { openBeta, promoteBeta } = await import(
'../../src/lib/server/fleet/beta.js'
);
const { createScript, getLatestVersion } =
await import('../../src/lib/server/fleet/scripts.js');
const { openBeta, promoteBeta } = await import('../../src/lib/server/fleet/beta.js');
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
const { db } = await import('../../src/lib/server/db/index.js');
await seedServerRegistry();
@@ -517,9 +731,9 @@ describe('missing artifact detection', () => {
waitForMatch: false
});
await applySource('beta', String(betaId));
db().prepare(
`UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?`
).run(new Date().toISOString(), betaId);
db()
.prepare(`UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?`)
.run(new Date().toISOString(), betaId);
db().prepare(`UPDATE betas SET state = 'ready-promote' WHERE id = ?`).run(betaId);
await promoteBeta(betaId);