import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { activateFiles } from '../helpers/staged-files.js'; let fixture: Fixture; const servers = ['t5_zombie_theater_1', 't5_zombie_cosmodrome_1']; const cfgPath = (server: string) => join(fixture.t5, server, 'configs', 'dedicated_sp.cfg'); beforeEach(async () => { vi.resetModules(); fixture = await newFixture(servers); vi.stubEnv('KOMODO_ADDRESS', ''); vi.stubEnv('RCON_PASSWORD', ''); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); for (const server of servers) await mkdir(join(fixture.t5, server, 'configs'), { recursive: true }); }); afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); }); describe('map selection through config lifecycle', () => { it('uses each server map on beta, promotion, and drift repair without map overlays', async () => { const { createScript, getLatestVersion } = await import('../../src/lib/server/fleet/scripts.js'); const { openBeta, promoteBeta } = await import('../../src/lib/server/fleet/beta.js'); const { scanServerDrift, runDriftDeployment } = await import('../../src/lib/server/fleet/drift.js'); const { db } = await import('../../src/lib/server/db/index.js'); const { script } = createScript({ name: 'baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\nset sv_maprotation "map zombie_moon"\n') }); const version = getLatestVersion(script.id)!; const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }], servers: [servers[0]], waitForHealth: false, waitForMatch: false }); await activateFiles('beta', betaId); expect(await readFile(cfgPath(servers[0]), 'utf8')).toContain('set sv_maprotation "map zombie_theater"'); 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); const promoted = await promoteBeta(betaId); expect(new Set(promoted.landed)).toEqual(new Set(servers)); await activateFiles('campaign', promoted.deploymentId); const ascension = await readFile(cfgPath(servers[1]), 'utf8'); expect(ascension).toContain('set sv_maprotation "map zombie_cosmodrome"'); expect(ascension).toContain('set sv_voice "1"'); for (const server of servers) expect(await scanServerDrift(server)).toEqual([]); await rm(cfgPath(servers[1])); const repaired = await runDriftDeployment({ servers: [servers[1]] }); expect(repaired.totalRestored).toBe(1); await activateFiles('campaign', repaired.deploymentId!); expect(await readFile(cfgPath(servers[1]), 'utf8')).toBe(ascension); expect(await scanServerDrift(servers[1])).toEqual([]); }); it('preserves unrelated legacy overlay settings and restores exact prior config on rollback', async () => { const { createScript, getLatestVersion } = await import('../../src/lib/server/fleet/scripts.js'); const { openBeta, rollbackBeta } = await import('../../src/lib/server/fleet/beta.js'); const { db } = await import('../../src/lib/server/db/index.js'); const baseline = createScript({ name: 'baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set sv_voice "1"\n') }).script; const baselineVersion = getLatestVersion(baseline.id)!; db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(baselineVersion.id, baseline.id); const overlay = createScript({ name: 'legacy map choice', kind: 'config', scopeType: 'map', scopeValue: 'zombie_cosmodrome', content: Buffer.from('set sv_maprotation "map zombie_moon"\nset g_inactivity "0"\n') }).script; const oldConfig = '// operator config\r\nset sv_maprotation "map zombie_cosmodrome"\r\n'; await writeFile(cfgPath(servers[1]), oldConfig); const { betaId } = await openBeta({ scripts: [{ scriptId: overlay.id, scriptVersionId: getLatestVersion(overlay.id)!.id }], servers: [servers[1]], waitForHealth: false, waitForMatch: false }); await activateFiles('beta', betaId); const preview = await readFile(cfgPath(servers[1]), 'utf8'); expect(preview).toContain('set sv_maprotation "map zombie_cosmodrome"'); expect(preview).toContain('set g_inactivity "0"'); expect(preview).not.toContain('map zombie_moon'); const rollback = await rollbackBeta(betaId); expect(await readFile(cfgPath(servers[1]), 'utf8')).toBe(preview); await activateFiles('campaign', rollback.deploymentId!); expect(await readFile(cfgPath(servers[1]), 'utf8')).toBe(oldConfig); }); it('keeps Plutainer default config during reconciliation until a baseline is promoted', async () => { const { scanServerDrift, runDriftDeployment } = await import('../../src/lib/server/fleet/drift.js'); const defaults = 'set sv_voice "1"\nset sv_maprotation "map zombie_theater"\n'; await writeFile(cfgPath(servers[1]), defaults); await writeFile(join(fixture.t5, servers[1], 'configs', 'stray.cfg'), 'set stray "1"'); const drift = await scanServerDrift(servers[1]); expect(drift.map((entry) => entry.relPath)).toEqual(['stray.cfg']); const driftResult = await runDriftDeployment({ servers: [servers[1]] }); await activateFiles('campaign', driftResult.deploymentId!); expect(await readFile(cfgPath(servers[1]), 'utf8')).toBe(defaults); }); });