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
+4
View File
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
import { readFile } 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'];
@@ -22,6 +23,7 @@ async function ready(kind: 'script' | 'baseline' = 'script', scope: 'game' | 'ma
scopeType: scope, scopeValue: scope === 'game' ? 't5' : 'zombie_theater', content: Buffer.from(kind === 'script' ? 'main(){}' : 'set sv_voice "1"\n') });
const version = getLatestVersion(script.id)!;
const { betaId } = await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }], servers: [servers[0]], waitForHealth: false, waitForMatch: false });
await activateFiles('beta', betaId);
db().prepare("UPDATE betas SET state = 'ready-promote' WHERE id = ?").run(betaId);
db().prepare("UPDATE beta_servers SET stage = 'ready', restarted_at = ? WHERE beta_id = ?").run(new Date().toISOString(), betaId);
return { betaId, script, version };
@@ -40,6 +42,8 @@ describe('reviewed beta promotion', () => {
const result = await promoteBeta(betaId, preview.token);
expect(result).toMatchObject({ awaitingActivation: 1, failedServers: [] });
expect(result.deploymentId).toContain('promote');
expect(existsSync(destination())).toBe(false);
await activateFiles('campaign', result.deploymentId);
expect(await readFile(destination(), 'utf8')).toBe('main(){}');
});
it('rejects changed scope membership before creating a campaign or writing extra files', async () => {