import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { existsSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { newFixture, type Fixture } from '../helpers/fixture.js'; vi.mock('node:fs', async (importOriginal) => { const original = await importOriginal(); return { ...original, writeFileSync: vi.fn(original.writeFileSync) }; }); vi.mock('../../src/lib/server/integrations/rcon.js', () => ({ rconGetPlayerCount: vi.fn(), rconStatusResult: vi.fn() })); let fixture: Fixture; const servers = ['t5_zombie_theater_1', 't5_zombie_theater_2']; const relPath = 'staged.gsc'; const before = Buffer.from('main(){ /* current live script */ }'); const after = Buffer.from('main(){ /* reviewed replacement */ }'); type Source = 'campaign' | 'beta'; const destination = (server = servers[0]) => join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', relPath); beforeEach(async () => { vi.resetModules(); vi.resetAllMocks(); const actualFs = await vi.importActual('node:fs'); vi.mocked(writeFileSync).mockImplementation(actualFs.writeFileSync); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(new Date('2026-09-27T15:00:00Z')); fixture = await newFixture(servers); vi.stubEnv('RCON_PASSWORD', 'test'); vi.stubEnv('DEPLOYMENT_RESTART_CONCURRENCY', '1'); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); const { rconGetPlayerCount, rconStatusResult } = await import('../../src/lib/server/integrations/rcon.js'); vi.mocked(rconGetPlayerCount).mockResolvedValue(0); vi.mocked(rconStatusResult).mockResolvedValue({ ok: true, endpoint: { host: 'test', port: 28960 }, rawReply: '', status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } }); }); afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); vi.useRealTimers(); }); async function queueChange(source: Source, targets = [servers[0]]) { const { db } = await import('../../src/lib/server/db/index.js'); const { createScript, uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js'); const { recordScript, getServerScript } = await import('../../src/lib/server/fleet/inventory.js'); const original = createScript({ name: 'Staged script', kind: 'script', targetRelPath: relPath, scopeType: 'game', scopeValue: 't5', content: before }); const replacement = uploadNewVersion(original.script.id, after); for (const server of targets) { await writeFile(destination(server), before); recordScript(server, relPath, original.version.id, original.version.sha256, before.length, 'fixture'); } const inventory = getServerScript(targets[0], relPath); let sourceId: string; if (source === 'campaign') { db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(replacement.id, original.script.id); const { runDriftDeployment } = await import('../../src/lib/server/fleet/drift.js'); const result = await runDriftDeployment({ servers: targets }); expect(result.deploymentId).toBeTruthy(); sourceId = result.deploymentId!; } else { const { openBeta } = await import('../../src/lib/server/fleet/beta.js'); const result = await openBeta({ scripts: [{ scriptId: original.script.id, scriptVersionId: replacement.id }], servers: targets, waitForHealth: false, waitForMatch: false }); sourceId = String(result.betaId); } return { sourceId, inventory, replacement }; } // File-storage unit tests exercise activation directly. Native provisioning and // safety suites cover admission, host delivery, player gates, and recovery. async function tick(source: Source) { const { db } = await import('../../src/lib/server/db/index.js'); const { activateFiles } = await import('../helpers/staged-files.js'); const sources = db().prepare('SELECT DISTINCT source_id FROM staged_file_changes WHERE source_kind=? AND state=\'pending\'').all(source) as { source_id: string }[]; for (const row of sources) await activateFiles(source, row.source_id); } async function expectLiveUnchanged(inventory: Awaited>['inventory'], server = servers[0]) { const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js'); expect(await readFile(destination(server))).toEqual(before); expect(getServerScript(server, relPath)).toEqual(inventory); } describe.each(['campaign', 'beta'])('%s staged file activation', (source) => { it('preserves existing file permissions when activating staged bytes', async () => { await queueChange(source); await chmod(destination(), 0o640); await tick(source); expect(await readFile(destination())).toEqual(after); expect((await stat(destination())).mode & 0o777).toBe(0o640); }); it('applies the exact queued bytes even if artifact storage changes while waiting', async () => { const { replacement } = await queueChange(source); const { db } = await import('../../src/lib/server/db/index.js'); db().prepare('UPDATE script_versions SET content = ? WHERE id = ?') .run(Buffer.from('changed after review'), replacement.id); await tick(source); expect(await readFile(destination())).toEqual(after); }); it('keeps the queued payload across an application restart', async () => { const { inventory } = await queueChange(source); await expectLiveUnchanged(inventory); const { closeDb } = await import('../../src/lib/server/db/index.js'); closeDb(); vi.resetModules(); await expectLiveUnchanged(inventory); await tick(source); expect(await readFile(destination())).toEqual(after); }); });