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
+10 -2
View File
@@ -2,6 +2,7 @@ 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'];
@@ -37,12 +38,14 @@ describe('map selection through config lifecycle', () => {
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"');
@@ -51,6 +54,7 @@ describe('map selection through config lifecycle', () => {
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([]);
});
@@ -75,11 +79,14 @@ describe('map selection through config lifecycle', () => {
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');
await rollbackBeta(betaId);
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);
});
@@ -90,7 +97,8 @@ describe('map selection through config lifecycle', () => {
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']);
await runDriftDeployment({ servers: [servers[1]] });
const driftResult = await runDriftDeployment({ servers: [servers[1]] });
await activateFiles('campaign', driftResult.deploymentId!);
expect(await readFile(cfgPath(servers[1]), 'utf8')).toBe(defaults);
});
});