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>
This commit is contained in:
2026-09-13 17:57:43 -04:00
co-authored by Claude Opus 4.7
parent 30b0d1d4da
commit 8c15cae80d
8 changed files with 154 additions and 163 deletions
+3 -3
View File
@@ -34,7 +34,7 @@ async function seedFleet(): Promise<void> {
}
describe('config baseline + overlay flow', () => {
it('composes baseline + overlay and writes dedicated_sp.cfg on beta open, restores .OLD on rollback', async () => {
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');
@@ -120,7 +120,7 @@ describe('config baseline + overlay flow', () => {
});
describe('plugin kind writes to plugin destination', () => {
it('deploys a plugin .dll under runtime/plutonium/plugins with -beta suffix', async () => {
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 });
@@ -144,7 +144,7 @@ describe('plugin kind writes to plugin destination', () => {
waitForMatch: false
});
const pluginPath = join(fixture.t5, 't5_kino_1', 'runtime/plutonium/plugins', 'demo-beta.dll');
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);
-18
View File
@@ -1,5 +1,4 @@
import { describe, expect, it } from 'vitest';
import { betaFile, oldFile } from '../../src/lib/server/fleet/files.js';
import { slugify } from '../../src/lib/server/fleet/scripts.js';
describe('slugify', () => {
@@ -9,20 +8,3 @@ describe('slugify', () => {
expect(slugify('t5_kino_1')).toBe('t5-kino-1');
});
});
describe('betaFile', () => {
it('inserts -beta before the extension', () => {
expect(betaFile('hud.gsc')).toBe('hud-beta.gsc');
expect(betaFile('perks/zom.gsc')).toBe('perks/zom-beta.gsc');
});
it('handles files without extension', () => {
expect(betaFile('README')).toBe('README-beta');
});
});
describe('oldFile', () => {
it('replaces extension with .OLD', () => {
expect(oldFile('hud.gsc')).toBe('hud.OLD');
expect(oldFile('perks/zom.gsc')).toBe('perks/zom.OLD');
});
});