Defer server file changes until guarded restart activation

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.
This commit is contained in:
2026-09-27 23:06:16 -04:00
parent eb2a852fdc
commit fb48f64de1
36 changed files with 1634 additions and 421 deletions
+15 -3
View File
@@ -3,6 +3,7 @@ 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';
@@ -39,20 +40,29 @@ describe('T6 config lifecycle in a mixed fleet', () => {
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));
expect((await runDriftDeployment({ servers: [farm] })).totalRestored).toBe(1);
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);
await rollbackPromoteDeployment(promoted.deploymentId);
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);
});
@@ -71,7 +81,9 @@ describe('T6 config lifecycle in a mixed fleet', () => {
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 rollbackBeta(betaId);
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] });