import { randomUUID } from 'node:crypto'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; vi.mock('../../src/lib/server/integrations/rcon.js', () => ({ rconStatusResult: vi.fn(async () => ({ ok: true, endpoint: { host: 'agent', port: 28960 }, rawReply: '', status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } })), rconGetPlayerCount: vi.fn(async () => 0), rconPlutoniumVersion: vi.fn(async () => ({ revision: 5354, error: null })) })); vi.mock('../../src/lib/server/pipeline/worker.js', () => ({ kick: vi.fn(async () => {}) })); let fixture: Fixture; let hostId: string; let instanceId: string; const server = 't5_zombie_theater_1'; const healthy = { exists: true, healthy: true, state: 'running' }; beforeEach(async () => { vi.resetModules(); vi.clearAllMocks(); fixture = await newFixture([]); vi.stubEnv('KOMODO_ADDRESS', ''); vi.stubEnv('KOMODO_API_KEY', ''); vi.stubEnv('KOMODO_API_SECRET', ''); const { createHost, createEnrollment, enrollHost } = await import('../../src/lib/server/hosts/index.js'); hostId = createHost({ name: 'Safety test host' }).id; enrollHost(createEnrollment(hostId).token, { protocolVersion: 1, dockerRunning: true, directoriesWritable: true, storageAvailable: 100_000_000_000, runtimeImages: ['plutainer:test'], libraries: [{ id: 'published-game-version', state: 'ready' }] }); const { db } = await import('../../src/lib/server/db/index.js'); instanceId = randomUUID(); db().prepare("INSERT INTO servers(name,komodo_name,game,map,game_mode,map_slug,game_port,container_state) VALUES(?,'','t5','zombie_theater','zombie','theater',28960,'running')").run(server); db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES(?,?,?,'published-game-version','plutainer:test',28960,?)`).run(instanceId, server, hostId, new Date().toISOString()); const backend = await import('../../src/lib/server/backends/native.js'); vi.spyOn(backend, 'nativeRequest').mockImplementation(async (_instance, kind) => { if (kind !== 'instance.inspect') throw new Error(`Unexpected remote mutation: ${kind}`); return healthy as never; }); }); afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); vi.restoreAllMocks(); }); async function advance() { const { processNativeProvisioning } = await import('../../src/lib/server/provisioning/native.js'); await processNativeProvisioning(() => null); } async function operationId(id: string) { const { db } = await import('../../src/lib/server/db/index.js'); return (db().prepare('SELECT operation_id FROM native_provisioning WHERE deployment_id=?').get(id) as { operation_id: string | null }).operation_id; } async function waitingRestart() { const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js'); const result = requestLifecycle(server, 'restart'); if (!result.ok) throw new Error(result.error); return result.deploymentId; } async function suspendInspection() { const { nativeRequest } = await import('../../src/lib/server/backends/native.js'); let release!: () => void; vi.mocked(nativeRequest).mockImplementationOnce(async () => { await new Promise(resolve => { release = resolve; }); return healthy as never; }); const running = advance(); await vi.waitFor(() => expect(release).toBeTypeOf('function')); return { running, release }; } async function stageCampaign() { const { db } = await import('../../src/lib/server/db/index.js'); db().prepare("INSERT INTO deployments(id,action,started_at) VALUES('campaign-race','promote',?)").run(new Date().toISOString()); db().prepare("INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('campaign-race',?,1,'pending')").run(server); const { queueFileChange } = await import('../../src/lib/server/fleet/staged-files.js'); queueFileChange({ sourceKind: 'campaign', sourceId: 'campaign-race', server, kind: 'script', relPath: 'hud.gsc', content: Buffer.from('reviewed campaign bytes'), inventorySource: 'campaign:test' }); const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js'); const restart = requestRestart(server, { kind: 'campaign', id: 'campaign-race' }); await advance(); const staged = db().prepare('SELECT stage_operation_id FROM native_provisioning WHERE deployment_id=?').get(restart.id) as { stage_operation_id: string }; expect(staged.stage_operation_id).toBeTruthy(); const { reportHostOperation } = await import('../../src/lib/server/hosts/index.js'); reportHostOperation(hostId, staged.stage_operation_id, { state: 'completed', result: { state: 'staged' } }); return restart.id; } describe('native disruption safety at dispatch', () => { it('lets fresh occupied telemetry veto an empty RCON result and resumes after that evidence expires', async () => { const id = await waitingRestart(); const { saveTelemetry } = await import('../../src/lib/server/fleet/telemetry.js'); saveTelemetry({ event: 'game_start', server, map: 'zombie_theater', players: [{ name: 'Still playing', guid: 'player-1' }], receivedAt: new Date().toISOString() }); await advance(); expect(await operationId(id)).toBeNull(); const { db } = await import('../../src/lib/server/db/index.js'); expect(db().prepare("SELECT COUNT(*) AS count FROM ec_host_operations WHERE kind='instance.restart'").get()).toEqual({ count: 0 }); db().prepare('UPDATE server_telemetry SET received_at=? WHERE server=?').run('2000-01-01T00:00:00Z', server); await advance(); expect(await operationId(id)).toBeTypeOf('string'); }); it('holds destruction if a beta becomes active while remote inspection is pending', async () => { const { destroyServer } = await import('../../src/lib/server/provisioning/instances.js'); const removal = destroyServer(server); if (!removal.ok || !removal.deploymentId) throw new Error('Removal was not queued.'); const pending = await suspendInspection(); const { createScript } = await import('../../src/lib/server/fleet/scripts.js'); const { script, version } = createScript({ name: 'Race beta', kind: 'script', targetRelPath: 'beta.gsc', content: Buffer.from('main() {}'), scopeType: 'game', scopeValue: 't5' }); const { db } = await import('../../src/lib/server/db/index.js'); const betaId = db().prepare("INSERT INTO betas(script_id,script_version_id,trigger_kind,state) VALUES(?,?,'manual','running')").run(script.id, version.id).lastInsertRowid; db().prepare("INSERT INTO beta_servers(beta_id,server,had_original,stage) VALUES(?,?,0,'awaiting-empty')").run(betaId, server); pending.release(); await pending.running; expect(await operationId(removal.deploymentId)).toBeNull(); expect(db().prepare('SELECT name FROM servers WHERE name=?').get(server)).toEqual({ name: server }); expect(db().prepare("SELECT COUNT(*) AS count FROM ec_host_operations WHERE kind='instance.remove'").get()).toEqual({ count: 0 }); }); it.each(['cancel', 'change bytes'] as const)('does not activate staged files after a %s during remote inspection', async change => { const id = await stageCampaign(); const pending = await suspendInspection(); const { cancelStagedFiles, queueFileChange, stagedFileChanges } = await import('../../src/lib/server/fleet/staged-files.js'); if (change === 'cancel') cancelStagedFiles('campaign', 'campaign-race', server); else queueFileChange({ sourceKind: 'campaign', sourceId: 'campaign-race', server, kind: 'script', relPath: 'hud.gsc', content: Buffer.from('different unreviewed bytes'), inventorySource: 'campaign:test' }); pending.release(); await pending.running; expect(await operationId(id)).toBeNull(); expect(stagedFileChanges('campaign', 'campaign-race')[0].state).toBe(change === 'cancel' ? 'cancelled' : 'pending'); const { db } = await import('../../src/lib/server/db/index.js'); expect(db().prepare("SELECT COUNT(*) AS count FROM ec_host_operations WHERE kind='instance.restart'").get()).toEqual({ count: 0 }); }); it('does not dispatch an old generation if the instance changes during inspection', async () => { const id = await waitingRestart(); const pending = await suspendInspection(); const { db } = await import('../../src/lib/server/db/index.js'); db().prepare('UPDATE native_instances SET generation=2 WHERE id=?').run(instanceId); pending.release(); await pending.running; expect(await operationId(id)).toBeNull(); expect(db().prepare("SELECT COUNT(*) AS count FROM ec_host_operations WHERE kind='instance.restart'").get()).toEqual({ count: 0 }); }); });