import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { readFile, mkdir } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture(['t5_zombie_theater_1']); delete process.env.KOMODO_ADDRESS; delete process.env.RCON_PASSWORD; }); afterEach(async () => { await fixture.cleanup(); }); const BASELINE_CFG = [ '// GENERAL', 'set g_inactivity "190"', 'set sv_voice "1"', '', 'set sv_maprotation "map zombie_theater"' ].join('\n'); async function seedFleet(): Promise { const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); // Pre-create the configs directory so the config write path finds a // hospitable server tree. await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'configs'), { recursive: true }); } describe('config baseline + overlay flow', () => { it('composes baseline + overlay and writes dedicated_sp.cfg on beta open, restores pre-beta state on rollback', async () => { await seedFleet(); const { createScript, getLatestVersion } = await import('../../src/lib/server/fleet/scripts.js'); const { openBeta, rollbackBeta } = await import('../../src/lib/server/fleet/beta.js'); // 1. Upload baseline for game=t5. const { script: baselineScript } = createScript({ name: 't5 baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from(BASELINE_CFG, 'utf8') }); const bVer = getLatestVersion(baselineScript.id)!; // 2. Upload an overlay for game=t5. const { script: overlayScript } = createScript({ name: 'disable afk', kind: 'config', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set g_inactivity "0"', 'utf8') }); const oVer = getLatestVersion(overlayScript.id)!; // 3. Bundle both into one beta. const { betaId } = await openBeta({ scripts: [ { scriptId: baselineScript.id, scriptVersionId: bVer.id }, { scriptId: overlayScript.id, scriptVersionId: oVer.id } ], servers: ['t5_zombie_theater_1'], waitForHealth: false, waitForMatch: false }); // 4. Verify: composed cfg lands at configs/dedicated_sp.cfg (in place, // NO -beta suffix — the game reads the exact filename). const cfgPath = join(fixture.t5, 't5_zombie_theater_1', 'configs', 'dedicated_sp.cfg'); expect(existsSync(cfgPath)).toBe(true); const composed = (await readFile(cfgPath)).toString('utf8'); expect(composed).toContain('set g_inactivity "0"'); expect(composed).not.toContain('set g_inactivity "190"'); // Baseline lines that weren't overridden are preserved. expect(composed).toContain('set sv_voice "1"'); // 5. Rollback restores the pre-beta state. There was no cfg on disk // beforehand, so rollback should just remove the beta-written file. await rollbackBeta(betaId); expect(existsSync(cfgPath)).toBe(false); }); it('requires an existing baseline before overlays can compose', async () => { await seedFleet(); const { createScript, getLatestVersion } = await import('../../src/lib/server/fleet/scripts.js'); const { openBeta } = await import('../../src/lib/server/fleet/beta.js'); const { db } = await import('../../src/lib/server/db/index.js'); const { script: overlayScript } = createScript({ name: 'lonely overlay', kind: 'config', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set g_inactivity "0"', 'utf8') }); const oVer = getLatestVersion(overlayScript.id)!; // openBeta returns success at the SQL level but the deploy step // marks the server 'failed' with a "no baseline" error because no // baseline is available. const { betaId } = await openBeta({ scripts: [{ scriptId: overlayScript.id, scriptVersionId: oVer.id }], servers: ['t5_zombie_theater_1'], waitForHealth: false, waitForMatch: false }); const row = db() .prepare(`SELECT stage, error FROM beta_servers WHERE beta_id = ? AND server = ?`) .get(betaId, 't5_zombie_theater_1') as { stage: string; error: string | null }; expect(row.stage).toBe('failed'); expect(row.error ?? '').toMatch(/no baseline available/); }); }); describe('plugin kind writes to plugin destination', () => { it('deploys a plugin .dll directly to its target filename', async () => { await seedFleet(); // Pre-create the plugins dir so the write can succeed. await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/plugins'), { recursive: true }); const { createScript, getLatestVersion } = await import('../../src/lib/server/fleet/scripts.js'); const { openBeta } = await import('../../src/lib/server/fleet/beta.js'); const { script } = createScript({ name: 'demo plugin', kind: 'plugin', releaseVersion: 'v3.1.0', targetRelPath: 'demo.dll', scopeType: 'game', scopeValue: 't5', content: Buffer.from([0x00, 0x01, 0x02, 0x03]) }); const v = getLatestVersion(script.id)!; const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: v.id }], servers: ['t5_zombie_theater_1'], waitForHealth: false, waitForMatch: false }); const pluginPath = join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/plugins', 'demo.dll'); expect(existsSync(pluginPath)).toBe(true); const bytes = await readFile(pluginPath); expect(bytes.equals(Buffer.from([0x00, 0x01, 0x02, 0x03]))).toBe(true); expect(betaId).toBeGreaterThan(0); const { GET } = await import('../../src/routes/api/betas/[id]/+server.js'); const detail = await (await GET({ params: { id: String(betaId) } })).json(); expect(detail.scripts[0].version.release_version).toBe('v3.1.0'); }); });