Files
electric-cherry/tests/unit/config-flow.test.ts
T
joshandClaude Opus 4.7 8c15cae80d Replace -beta sidecar and .OLD backup with DB snapshots
Beta deploys used to rename foo.gsc -> foo.OLD and write beta bytes to
a foo-beta.gsc sidecar, forcing the mod loader to know the naming
convention. Now the beta content lives at the actual target filename
(same as configs already did) and the pre-beta bytes are captured in
a new beta_file_snapshots table for rollback.

- schema.sql: new beta_file_snapshots(beta_id, server, rel_path,
  prior_content BLOB, prior_sha256, prior_size). Handles scripts,
  plugins, and the composed dedicated_sp.cfg uniformly.
- db/index.ts: assertSnapshotSchemeSafe boot guard refuses to start
  if a legacy running/ready-promote beta is present on first upgrade.
  A settings sentinel (snapshots_v1) makes it one-shot.
- beta.ts openBeta: read prior bytes, insert snapshot, overwrite
  target directly.
- beta.ts rollbackBeta: restore from snapshot if had_original, else
  delete the fresh file. Drop snapshot rows after restore.
- beta.ts promoteBeta: no more rm -beta / rm .OLD calls, pull prior
  content from the snapshot table for the deployment audit, drop
  snapshots after promote.
- files.ts: delete betaFile() and oldFile().
- drift.ts: docstring updated; no special-casing needed since beta
  bytes and inventory now agree on the target rel_path.
- servers/[name]/+page.svelte: drop dead -beta/.OLD row filters.
- tests: plugin write asserts the target filename; unused
  betaFile/oldFile tests removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-13 17:57:43 -04:00

154 lines
6.1 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';
let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture(['t5_kino_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_kino_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_kino_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_kino_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_kino_1'],
waitForHealth: false,
waitForMatch: false
});
const row = db()
.prepare(`SELECT stage, error FROM beta_servers WHERE beta_id = ? AND server = ?`)
.get(betaId, 't5_kino_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_kino_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',
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_kino_1'],
waitForHealth: false,
waitForMatch: false
});
const pluginPath = join(fixture.t5, 't5_kino_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);
});
});