Persist beta, promotion, drift, and rollback payloads until occupancy and restart-capacity checks pass. Preserve rollback snapshots and campaign ordering, and allow held rollback campaigns to resume safely. Fixes #28 Validation: 805 tests passed, Svelte checks clean, production build passed.
165 lines
6.8 KiB
TypeScript
165 lines
6.8 KiB
TypeScript
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';
|
|
import { activateFiles } from '../helpers/staged-files.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<void> {
|
|
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 for activation and queues restoration of the pre-beta state', 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
|
|
});
|
|
|
|
// The live filename is untouched until activation.
|
|
const cfgPath = join(fixture.t5, 't5_zombie_theater_1', 'configs', 'dedicated_sp.cfg');
|
|
expect(existsSync(cfgPath)).toBe(false);
|
|
await activateFiles('beta', betaId);
|
|
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.
|
|
const rollback = await rollbackBeta(betaId);
|
|
expect(await readFile(cfgPath, 'utf8')).toBe(composed);
|
|
await activateFiles('campaign', rollback.deploymentId!);
|
|
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
|
|
});
|
|
await activateFiles('beta', betaId);
|
|
|
|
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(false);
|
|
await activateFiles('beta', betaId);
|
|
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');
|
|
});
|
|
});
|