import { createHash, randomUUID } from 'node:crypto'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; const server = 't5_zombie_theater_1'; const scriptRoot = 'runtime/plutonium/storage/t5/raw/scripts/sp'; const pluginRoot = 'runtime/plutonium/plugins'; const hash = (content: Buffer) => createHash('sha256').update(content).digest('hex'); let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { await fixture.cleanup(); vi.restoreAllMocks(); }); async function setup(capable = true) { const hosts = await import('../../src/lib/server/hosts/index.js'); const host = hosts.createHost({ name: 'Drift manifest host' }); hosts.enrollHost(hosts.createEnrollment(host.id).token, { protocolVersion: 1, capabilities: capable ? ['files.manifest'] : [] }); const { db } = await import('../../src/lib/server/db/index.js'); const database = db(); database .prepare( `INSERT INTO servers(name,game,map,game_mode,map_slug,created_by) VALUES(?,'t5','zombie_theater','zombie','theater','ui')` ) .run(server); database .prepare( `INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES(?,?,?,'library','image',28960,?)` ) .run(randomUUID(), server, host.id, new Date().toISOString()); database .prepare( `INSERT INTO ec_host_operations(id,host_id,instance_id,generation,kind,payload,state,created_at,updated_at) SELECT 'launch',host_id,id,generation,'instance.create','{}','completed',created_at,created_at FROM native_instances WHERE server=?` ) .run(server); const { createScript } = await import('../../src/lib/server/fleet/scripts.js'); const { recordScript } = await import('../../src/lib/server/fleet/inventory.js'); const contents = new Map(); function promoted( kind: 'script' | 'plugin' | 'baseline', name: string, content: Buffer, targetRelPath?: string ) { const artifact = createScript({ releaseVersion: '1.0.0', name, kind, targetRelPath, scopeType: 'game', scopeValue: 't5', content }); database .prepare('UPDATE scripts SET current_version_id=? WHERE id=?') .run(artifact.version.id, artifact.script.id); return artifact; } const script = promoted( 'script', 'Managed script', Buffer.from('main() { wait 1; }'), 'managed.gsc' ); const plugin = promoted( 'plugin', 'Managed plugin', Buffer.from('promoted plugin'), 'managed.dll' ); const baseline = promoted('baseline', 'Baseline', Buffer.from('set sv_voice "1"\n')); for (const [root, artifact] of [ [scriptRoot, script], [pluginRoot, plugin] ] as const) { const { version } = artifact; contents.set( `${root}/${artifact.script.target_rel_path}`, (await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(version.id) ); recordScript( server, artifact.script.target_rel_path, version.id, version.sha256, version.size, 'test' ); } const { getPromotedServerConfig, getServerMapConfig, MAP_CONFIG_FILE } = await import('../../src/lib/server/fleet/server-config.js'); const config = getPromotedServerConfig('t5', 'zombie_theater', 'theater')!; contents.set('configs/dedicated_sp.cfg', config); recordScript(server, 'dedicated_sp.cfg', null, hash(config), config.length, 'test'); contents.set( `configs/${MAP_CONFIG_FILE}`, getServerMapConfig('t5', 'zombie_theater', 'theater')! ); // The runtime owns this generated file; neither scanner should classify it as drift. contents.set('configs/dedicated.cfg', Buffer.from('runtime generated')); const backend = await import('../../src/lib/server/backends/native.js'); const request = vi .spyOn(backend, 'nativeRequest') .mockImplementation(async (_instance, kind, payload = {}) => { if (kind === 'files.manifest') { const roots = payload.roots as string[]; const ignored = payload.ignoreNames as string[]; return { files: [...contents] .filter( ([path]) => roots.some((root) => path.startsWith(`${root}/`)) && !path.split('/').some((part) => ignored.includes(part)) ) .map(([path, content]) => ({ path, size: content.length, sha256: hash(content) })) } as never; } if (capable) throw new Error(`Manifest scan requested an extra remote operation: ${kind}`); const path = String(payload.path); const content = contents.get(path); if (kind === 'files.read') return { contentBase64: content?.toString('base64') ?? null } as never; if (kind === 'files.stat') { const isDirectory = path === '.' || [...contents.keys()].some((file) => file.startsWith(`${path}/`)); return { exists: content !== undefined || isDirectory, size: content?.length ?? 0, mtime: new Date().toISOString(), isDirectory, isFile: content !== undefined } as never; } if (kind === 'files.list') { const entries = new Map< string, { name: string; isDirectory: boolean; isFile: boolean } >(); for (const file of contents.keys()) { if (!file.startsWith(`${path}/`)) continue; const [name, ...nested] = file.slice(path.length + 1).split('/'); entries.set(name, { name, isDirectory: nested.length > 0, isFile: nested.length === 0 }); } return { entries: [...entries.values()] } as never; } throw new Error(`Unexpected remote operation: ${kind}`); }); const drift = await import('../../src/lib/server/fleet/drift.js'); return { ...drift, database, request, contents, script, plugin, baseline, promoted, mapConfigFile: MAP_CONFIG_FILE }; } function expectOneManifest(request: Awaited>['request']) { expect(request).toHaveBeenCalledTimes(1); expect(request.mock.calls[0][1]).toBe('files.manifest'); expect(request.mock.calls[0][2]).toEqual({ roots: [scriptRoot, 'configs', pluginRoot], ignoreNames: ['dedicated.cfg'], paths: [ `${scriptRoot}/managed.gsc`, `${pluginRoot}/managed.dll`, 'configs/dedicated_sp.cfg', 'configs/electric_cherry_map.cfg' ] }); } describe('native drift manifests', () => { it.each([true, false])( 'reconciles verified startup password changes without writing server files (manifest: %s)', async (capable) => { const api = await setup(capable); const { applyRuntimePassword } = await import('../../src/lib/server/fleet/runtime-config.js'); const { getServerScript, recordScript } = await import('../../src/lib/server/fleet/inventory.js'); const promoted = Buffer.from( 'set rcon_password "password"\nset sv_maprotation "map zombie_theater"\n' ); const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js'); // Replace the promoted baseline bytes, retaining a legacy inventory hash. const next = uploadNewVersion(api.baseline.script.id, promoted); api.database .prepare('UPDATE scripts SET current_version_id=? WHERE id=?') .run(next.id, api.baseline.script.id); const password = 'a'.repeat(64); api.database .prepare('UPDATE ec_host_operations SET payload=? WHERE id=?') .run(JSON.stringify({ rconPassword: password }), 'launch'); recordScript( server, 'dedicated_sp.cfg', null, hash(promoted), promoted.length, 'native-create' ); const effective = Buffer.from(applyRuntimePassword(promoted.toString(), password)); expect(effective.length - promoted.length).toBe(56); api.contents.set('configs/dedicated_sp.cfg', effective); expect(await api.scanServerDrift(server)).toEqual([]); expect(getServerScript(server, 'dedicated_sp.cfg')).toMatchObject({ sha256: hash(effective), size: effective.length, source: 'runtime-config-reconciliation' }); if (capable) expectOneManifest(api.request); expect((await api.prepareDriftCampaign(server)).plan.files).toEqual([]); // A wrong password of the same length is still real drift. api.contents.set( 'configs/dedicated_sp.cfg', Buffer.from(applyRuntimePassword(promoted.toString(), 'b'.repeat(64))) ); expect(await api.scanServerDrift(server)).toEqual([ expect.objectContaining({ relPath: 'dedicated_sp.cfg', driftKind: 'replace' }) ]); if (!capable) { const repair = await api.prepareDriftCampaign(server); expect(repair.files[0].after).toEqual(effective); } api.contents.set( 'configs/dedicated_sp.cfg', Buffer.concat([effective, Buffer.from('set sv_voice "0"\n')]) ); expect(await api.scanServerDrift(server)).toHaveLength(1); } ); it('reports unavailable credentials as a scan failure rather than scheduling a destructive repair', async () => { const api = await setup(); api.database .prepare( "UPDATE ec_host_operations SET history_expired_at='expired',payload='{}' WHERE id='launch'" ) .run(); await expect(api.scanServerDrift(server)).rejects.toThrow( 'launch credentials are unavailable' ); }); it('checks managed files, composed config, generated config, and untracked trees in one request', async () => { const api = await setup(); expect(await api.scanServerDrift(server)).toEqual([]); expectOneManifest(api.request); api.request.mockClear(); expect(await api.prepareDriftCampaign(server)).toMatchObject({ plan: { files: [] }, files: [] }); expectOneManifest(api.request); }); it('detects changed, missing, and nested untracked files from the same manifest', async () => { const api = await setup(); api.contents.set(`${scriptRoot}/managed.gsc`, Buffer.alloc(0)); api.contents.delete(`${pluginRoot}/managed.dll`); api.contents.set('configs/dedicated_sp.cfg', Buffer.from('changed config')); const stray = Buffer.from('untracked bytes'); api.contents.set(`${scriptRoot}/custom/stray.gsc`, stray); const changes = await api.scanServerDrift(server); expect(changes).toHaveLength(4); expect(changes).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 'script', relPath: 'managed.gsc', driftKind: 'replace', sha256: api.script.version.sha256 }), expect.objectContaining({ kind: 'plugin', relPath: 'managed.dll', driftKind: 'missing', sha256: api.plugin.version.sha256 }), expect.objectContaining({ kind: 'config', relPath: 'dedicated_sp.cfg', driftKind: 'replace' }), expect.objectContaining({ kind: 'script', relPath: 'custom/stray.gsc', driftKind: 'untracked', size: stray.length, sha256: hash(stray) }) ]) ); expectOneManifest(api.request); }); it('still repairs missing inventory and generated map config', async () => { const api = await setup(); api.database .prepare('DELETE FROM server_scripts WHERE server=? AND rel_path=?') .run(server, 'managed.gsc'); api.contents.delete(`configs/${api.mapConfigFile}`); expect(await api.scanServerDrift(server)).toEqual( expect.arrayContaining([ expect.objectContaining({ relPath: 'managed.gsc', driftKind: 'replace' }), expect.objectContaining({ relPath: api.mapConfigFile, driftKind: 'missing' }) ]) ); expectOneManifest(api.request); }); it('compares explicitly managed ignored filenames without treating runtime-owned files as untracked', async () => { const api = await setup(); api.promoted('plugin', 'Managed ignored name', Buffer.from('expected'), 'dedicated.cfg'); api.contents.set(`${pluginRoot}/dedicated.cfg`, Buffer.from('changed')); expect(await api.scanServerDrift(server)).toEqual([ expect.objectContaining({ kind: 'plugin', relPath: 'dedicated.cfg', driftKind: 'replace' }) ]); expect(api.request).toHaveBeenCalledTimes(1); expect(api.request.mock.calls[0][2]).toMatchObject({ ignoreNames: [], paths: expect.arrayContaining([`${pluginRoot}/dedicated.cfg`]) }); }); it.each(['running', 'ready-promote'])( 'skips remote reads while a %s beta owns the server', async (state) => { const api = await setup(); const beta = api.database .prepare( "INSERT INTO betas(script_id,script_version_id,trigger_kind,state) VALUES(?,?,'manual',?)" ) .run(api.script.script.id, api.script.version.id, state).lastInsertRowid; api.database .prepare( "INSERT INTO beta_servers(beta_id,server,had_original,stage) VALUES(?,?,0,'awaiting-empty')" ) .run(beta, server); expect(await api.scanServerDrift(server)).toEqual([]); expect(api.request).not.toHaveBeenCalled(); } ); it('preserves unavailable evidence as a failed scan without falling back to per-file reads', async () => { const api = await setup(); api.request.mockRejectedValueOnce(new Error('Agent manifest unavailable')); await expect(api.scanServerDrift(server)).rejects.toThrow('Agent manifest unavailable'); expectOneManifest(api.request); }); it('rejects a malformed manifest instead of treating it as an empty tree', async () => { const api = await setup(); api.request.mockResolvedValueOnce({} as never); await expect(api.scanServerDrift(server)).rejects.toThrow(); expectOneManifest(api.request); }); it('keeps legacy agents on the existing per-file scan and detects their drift', async () => { const api = await setup(false); api.contents.set(`${scriptRoot}/managed.gsc`, Buffer.from('old script')); api.contents.set('configs/stray.cfg', Buffer.from('untracked config')); expect(await api.scanServerDrift(server)).toEqual( expect.arrayContaining([ expect.objectContaining({ relPath: 'managed.gsc', driftKind: 'replace' }), expect.objectContaining({ relPath: 'stray.cfg', driftKind: 'untracked' }) ]) ); const operations = api.request.mock.calls.map(([, kind]) => kind); expect(operations).not.toContain('files.manifest'); expect(operations).toEqual( expect.arrayContaining(['files.stat', 'files.list', 'files.read']) ); }); });