103 lines
6.2 KiB
TypeScript
103 lines
6.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { existsSync } from 'node:fs';
|
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { join, resolve } from 'node:path';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import { activateFiles } from '../helpers/staged-files.js';
|
|
|
|
let fixture: Fixture;
|
|
const town = 't6_zombie_town_1';
|
|
const farm = 't6_zombie_farm_1';
|
|
const t5 = 't5_zombie_theater_1';
|
|
const cfgPath = (server: string) => join(fixture.t6, server, 'configs/dedicated_zm.cfg');
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture([town, farm, t5]);
|
|
|
|
vi.stubEnv('RCON_PASSWORD', '');
|
|
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
|
await seedServerRegistry();
|
|
for (const name of [town, farm]) await mkdir(join(fixture.t6, name, 'configs'), { recursive: true });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fixture.cleanup();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('T6 config lifecycle in a mixed fleet', () => {
|
|
it('preserves variants through beta, scope promotion, drift repair, and campaign rollback', async () => {
|
|
const { createScript } = 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 { rollbackPromoteDeployment } = await import('../../src/lib/server/fleet/rollback.js');
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const previous = '// prior operator settings\r\nsv_voice 0\r\n';
|
|
await writeFile(cfgPath(town), previous);
|
|
const { script, version } = createScript({ name: 'T6 baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't6',
|
|
content: Buffer.from('sv_voice 1\nsv_maprotation "map zm_buried"\n') });
|
|
expect(script.target_rel_path).toBe('dedicated_zm.cfg');
|
|
const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }],
|
|
servers: [town], waitForHealth: false, waitForMatch: false });
|
|
expect(await readFile(cfgPath(town), 'utf8')).toBe(previous);
|
|
await activateFiles('beta', betaId);
|
|
expect(await readFile(cfgPath(town), 'utf8')).toContain('execgts zm_standard_town.cfg map zm_transit');
|
|
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([town, farm]));
|
|
expect(existsSync(cfgPath(farm))).toBe(false);
|
|
await activateFiles('campaign', promoted.deploymentId);
|
|
const farmConfig = await readFile(cfgPath(farm), 'utf8');
|
|
expect(farmConfig).toContain('execgts zm_standard_farm.cfg map zm_transit');
|
|
expect(existsSync(join(fixture.t5, t5, 'configs/dedicated_sp.cfg'))).toBe(false);
|
|
expect(existsSync(join(fixture.t6, farm, 'configs/dedicated_sp.cfg'))).toBe(false);
|
|
expect(await scanServerDrift(farm)).toEqual([]);
|
|
await rm(cfgPath(farm));
|
|
const drift = await runDriftDeployment({ servers: [farm] });
|
|
expect(drift.totalRestored).toBe(1);
|
|
expect(existsSync(cfgPath(farm))).toBe(false);
|
|
await activateFiles('campaign', drift.deploymentId!);
|
|
expect(await readFile(cfgPath(farm), 'utf8')).toBe(farmConfig);
|
|
const rollback = await rollbackPromoteDeployment(promoted.deploymentId);
|
|
expect(await readFile(cfgPath(farm), 'utf8')).toBe(farmConfig);
|
|
await activateFiles('campaign', rollback.deploymentId);
|
|
expect(await readFile(cfgPath(town), 'utf8')).toBe(previous);
|
|
expect(existsSync(cfgPath(farm))).toBe(false);
|
|
});
|
|
|
|
it('restores the exact pre-beta T6 config and preserves seeded rulesets during drift repair', async () => {
|
|
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
|
const { openBeta, rollbackBeta } = await import('../../src/lib/server/fleet/beta.js');
|
|
const { scanServerDrift, runDriftDeployment } = await import('../../src/lib/server/fleet/drift.js');
|
|
const previous = 'sv_voice 1\n';
|
|
await writeFile(cfgPath(town), previous);
|
|
const settings = join(fixture.t6, town, 'runtime/plutonium/storage/t6/gamesettings');
|
|
await mkdir(settings, { recursive: true });
|
|
await writeFile(join(settings, 'zm_standard_town.cfg'), 'ui_zm_mapstartlocation town\n');
|
|
await writeFile(join(fixture.t6, town, 'configs/dedicated.cfg'), 'sv_voice 1\n');
|
|
const { script, version } = createScript({ name: 'T6 baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't6',
|
|
content: Buffer.from('sv_voice 0\n') });
|
|
const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }],
|
|
servers: [town], waitForHealth: false, waitForMatch: false });
|
|
await activateFiles('beta', betaId);
|
|
const rollback = await rollbackBeta(betaId);
|
|
await activateFiles('campaign', rollback.deploymentId!);
|
|
expect(await readFile(cfgPath(town), 'utf8')).toBe(previous);
|
|
expect(await scanServerDrift(town)).toEqual([]);
|
|
await runDriftDeployment({ servers: [town] });
|
|
expect(await readFile(join(settings, 'zm_standard_town.cfg'), 'utf8')).toBe('ui_zm_mapstartlocation town\n');
|
|
});
|
|
|
|
it('reads the T6 game log and keeps log path traversal checks', async () => {
|
|
const { resolveLogPath, gameLogKey, tailLog } = await import('../../src/lib/server/fleet/logs.js');
|
|
const logs = join(fixture.t6, town, 'logs');
|
|
await mkdir(logs, { recursive: true });
|
|
await writeFile(join(logs, 'games_zm.log'), '0:00 InitGame:\n0:01 J;123;0;Player\n');
|
|
expect(resolveLogPath(town, gameLogKey(town))).toBe(resolve(logs, 'games_zm.log'));
|
|
expect((await tailLog(resolveLogPath(town, gameLogKey(town))!, 10))?.lines).toContain('0:01 J;123;0;Player');
|
|
expect(resolveLogPath('t6_/../../escape', 'games_zm')).toBeNull();
|
|
});
|
|
});
|