diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 6c4b9dc..63f1ab3 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -149,6 +149,30 @@ function dropLegacyBetaTables(handle: Database.Database): void { } } +// One-shot guard for the -beta/.OLD → DB-snapshot cutover. Legacy running/ +// ready-promote betas from the old code have -beta. and .OLD files on +// disk with no corresponding beta_file_snapshots rows — restoring them under +// the new rollback path would delete-instead-of-restore. If a legacy active +// beta is detected on the first boot after upgrade, refuse to start so the +// operator can rollback/promote under the old code. A settings sentinel marks +// the cutover complete so subsequent boots with (legitimately new-scheme) +// active betas aren't blocked. +function assertSnapshotSchemeSafe(handle: Database.Database): void { + const done = handle.prepare("SELECT value FROM settings WHERE key = 'snapshots_v1'").get() as + | { value: string } + | undefined; + if (done) return; + const active = handle + .prepare("SELECT id FROM betas WHERE state IN ('running','ready-promote') LIMIT 1") + .get() as { id: number } | undefined; + if (active) { + throw new Error( + `close active beta ${active.id} before upgrading — snapshot scheme is incompatible with legacy -beta/.OLD state` + ); + } + handle.prepare("INSERT INTO settings (key, value) VALUES ('snapshots_v1', '1')").run(); +} + export function db(): Database.Database { if (_db) return _db; @@ -179,6 +203,7 @@ export function db(): Database.Database { ); handle.exec(`CREATE INDEX IF NOT EXISTS idx_scripts_kind ON scripts(kind)`); backfillMultiScriptJoins(handle); + assertSnapshotSchemeSafe(handle); } catch (err) { handle.close(); throw err; diff --git a/src/lib/server/db/schema.sql b/src/lib/server/db/schema.sql index 7e92349..c995b2f 100644 --- a/src/lib/server/db/schema.sql +++ b/src/lib/server/db/schema.sql @@ -123,8 +123,9 @@ CREATE INDEX IF NOT EXISTS idx_beta_scripts_beta ON beta_scripts(beta_id); CREATE INDEX IF NOT EXISTS idx_beta_scripts_script ON beta_scripts(script_id); -- Per (beta, server, script) prior-file state. A server row in beta_servers --- covers restart progress; this table covers what each script's target --- looked like on that server before the beta wrote its -beta.gsc. +-- covers restart progress; this table covers whether each script's target +-- existed on that server before the beta overwrote it — snapshot content +-- lives in beta_file_snapshots. CREATE TABLE IF NOT EXISTS beta_server_scripts ( beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE, server TEXT NOT NULL REFERENCES servers(name), @@ -135,6 +136,21 @@ CREATE TABLE IF NOT EXISTS beta_server_scripts ( ); CREATE INDEX IF NOT EXISTS idx_beta_server_scripts_beta ON beta_server_scripts(beta_id); +-- Pre-beta bytes of every target file the beta overwrote on each server. +-- Absence of a row for (beta, server, rel_path) means the file did not exist +-- before the beta wrote it — rollback deletes the file rather than restoring. +-- Handles scripts, plugins, and the composed dedicated_sp.cfg uniformly. +CREATE TABLE IF NOT EXISTS beta_file_snapshots ( + beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE, + server TEXT NOT NULL REFERENCES servers(name) ON DELETE CASCADE, + rel_path TEXT NOT NULL, + prior_content BLOB NOT NULL, + prior_sha256 TEXT NOT NULL, + prior_size INTEGER NOT NULL, + PRIMARY KEY (beta_id, server, rel_path) +); +CREATE INDEX IF NOT EXISTS idx_beta_file_snapshots_beta ON beta_file_snapshots(beta_id); + CREATE TABLE IF NOT EXISTS deployments ( id TEXT PRIMARY KEY, action TEXT NOT NULL, -- 'drift' | 'promote' | 'rollback' diff --git a/src/lib/server/fleet/beta.ts b/src/lib/server/fleet/beta.ts index 361b154..e5cac32 100644 --- a/src/lib/server/fleet/beta.ts +++ b/src/lib/server/fleet/beta.ts @@ -1,9 +1,9 @@ -import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { dirname, join, posix } from 'node:path'; import { betaArchiveOf, config, destOf, destOfKind, type ArtifactKind } from '../config.js'; import { db } from '../db/index.js'; -import { betaFile, oldFile, sha256Buffer } from './files.js'; +import { sha256Buffer } from './files.js'; import { ensureDest, getServer, heldByBeta } from './servers.js'; import { getBaselineForScope, @@ -338,24 +338,18 @@ export async function openBeta(input: OpenBetaInput): Promise { throw new BetaError('one or more servers are out of scope', 'out-of-scope', outOfScope); } - // Per-(server, script) conflict + stale-.OLD check. + // Per-(server, script) conflict check. heldByBeta covers "another running + // beta already claimed this target"; the previous stale-.OLD check is no + // longer relevant now that snapshots live in the DB. const conflicts: Array<{ server: string; scriptId: number; heldByBetaId: number }> = []; - const blocked: Array<{ server: string; ofile: string }> = []; for (const name of input.servers) { const held = heldByBeta(name); for (const { script } of resolved) { const owner = held.get(script.target_rel_path); if (owner) conflicts.push({ server: name, scriptId: script.id, heldByBetaId: owner }); - const dest = destOfKind(name, script.kind); - const targetPath = join(dest, script.target_rel_path); - const backupPath = join(dest, oldFile(script.target_rel_path)); - if (existsSync(targetPath) && existsSync(backupPath)) { - blocked.push({ server: name, ofile: oldFile(script.target_rel_path) }); - } } } if (conflicts.length > 0) throw new BetaError('target already held by another beta', 'conflict', conflicts); - if (blocked.length > 0) throw new BetaError('stale .OLD backups in the way', 'blocked-backup', blocked); // Ensure destinations exist. The `ensureDest` helper covers only the // script destination today; config/plugin destinations are created @@ -417,15 +411,20 @@ export async function openBeta(input: OpenBetaInput): Promise { const collapsingOverlays = resolved.filter((r) => r.script.kind === 'config'); const collapsingBaseline = resolved.find((r) => r.script.kind === 'baseline'); - // Archive: one -beta.gsc per non-collapsing artifact, plus one composed - // preview per beta server for the config layer. + // Archive: one copy per non-collapsing artifact under its target basename, + // plus one composed preview per beta server for the config layer. const archive = betaArchiveOf(betaId); await mkdir(archive, { recursive: true }); for (const r of nonCollapsing) { - const bfile = betaFile(r.script.target_rel_path); - await writeFile(join(archive, posix.basename(bfile)), r.version.content); + const base = posix.basename(r.script.target_rel_path); + await writeFile(join(archive, base), r.version.content); } + const insertSnapshot = db().prepare( + `INSERT INTO beta_file_snapshots (beta_id, server, rel_path, prior_content, prior_sha256, prior_size) + VALUES (?, ?, ?, ?, ?, ?)` + ); + // On-disk deploy per server. If any write fails on a server, mark the // whole server 'failed' — we can't restart a partially deployed server // safely. Remaining artifacts on that server are skipped. @@ -438,35 +437,35 @@ export async function openBeta(input: OpenBetaInput): Promise { for (const name of input.servers) { const serverRow = serverRows.find((r) => r.name === name)!; let firstErr: string | null = null; - // 1. Non-collapsing artifacts: script/plugin — one file each. + // 1. Non-collapsing artifacts: script/plugin — write directly to the + // target filename. Pre-beta bytes go into beta_file_snapshots so + // rollback can restore them. for (const { script, version } of nonCollapsing) { if (firstErr) break; const dest = destOfKind(name, script.kind); - const bfile = betaFile(script.target_rel_path); - const ofile = oldFile(script.target_rel_path); const targetPath = join(dest, script.target_rel_path); - const backupPath = join(dest, ofile); - const betaPath = join(dest, bfile); try { await mkdir(join(dest, dirname(script.target_rel_path)), { recursive: true }); if (existsSync(targetPath)) { - await rename(targetPath, backupPath); - const prior = getServerScript(name, script.target_rel_path); - removeScript(name, script.target_rel_path); - if (prior) { - recordScript(name, ofile, prior.script_version_id, prior.sha256, prior.size, `beta:${betaId}`); - } + const priorBytes = await readFile(targetPath); + insertSnapshot.run( + betaId, + name, + script.target_rel_path, + priorBytes, + sha256Buffer(priorBytes), + priorBytes.byteLength + ); } - await writeFile(betaPath, version.content); - recordScript(name, bfile, version.id, version.sha256, version.size, `beta:${betaId}`); + await writeFile(targetPath, version.content); + recordScript(name, script.target_rel_path, version.id, version.sha256, version.size, `beta:${betaId}`); } catch (err) { firstErr = `${script.name}: ${(err as Error).message}`; } } // 2. Collapsing layer: compose the effective dedicated_sp.cfg from - // baseline + all applicable overlays, write in place (no -beta - // suffix — the game reads the exact filename), backup the prior - // file as .OLD once. + // baseline + all applicable overlays, write in place. Pre-beta cfg + // bytes go into beta_file_snapshots keyed on the target rel_path. const hasCollapsing = collapsingBaseline || collapsingOverlays.length > 0; if (!firstErr && hasCollapsing) { try { @@ -483,14 +482,16 @@ export async function openBeta(input: OpenBetaInput): Promise { await mkdir(dest, { recursive: true }); const targetRel = config.configTargetFile; const targetPath = join(dest, targetRel); - const backupPath = join(dest, oldFile(targetRel)); if (existsSync(targetPath)) { - await rename(targetPath, backupPath); - const prior = getServerScript(name, targetRel); - removeScript(name, targetRel); - if (prior) { - recordScript(name, oldFile(targetRel), prior.script_version_id, prior.sha256, prior.size, `beta:${betaId}`); - } + const priorBytes = await readFile(targetPath); + insertSnapshot.run( + betaId, + name, + targetRel, + priorBytes, + sha256Buffer(priorBytes), + priorBytes.byteLength + ); } const composedBuf = Buffer.from(composedResult.text, 'utf8'); await writeFile(targetPath, composedBuf); @@ -563,44 +564,38 @@ export async function rollbackBeta(betaId: number): Promise { const hasCollapsing = bundle.some((b) => isCollapsingKind(b.script.kind)); + const getSnapshot = db().prepare( + `SELECT prior_content, prior_sha256, prior_size FROM beta_file_snapshots + WHERE beta_id = ? AND server = ? AND rel_path = ?` + ); + for (const bs of servers) { for (const { script } of bundle) { if (isCollapsingKind(script.kind)) continue; // handled once per server below const dest = destOfKind(bs.server, script.kind); - const bfile = betaFile(script.target_rel_path); - const ofile = oldFile(script.target_rel_path); - const betaPath = join(dest, bfile); - const backupPath = join(dest, ofile); const targetPath = join(dest, script.target_rel_path); - // Remove -beta. regardless of stage. - if (existsSync(betaPath)) await rm(betaPath, { force: true }); - removeScript(bs.server, bfile); - const bss = getBetaServerScript(betaId, bs.server, script.id); if (bss?.had_original) { - if (existsSync(backupPath)) { - await rename(backupPath, targetPath); - removeScript(bs.server, ofile); - const prior = bss.prior_version_id ? getVersion(bss.prior_version_id) : null; - if (prior) { - recordScript(bs.server, script.target_rel_path, prior.id, prior.sha256, prior.size, `rollback:${betaId}`); + const snap = getSnapshot.get(betaId, bs.server, script.target_rel_path) as + | { prior_content: Buffer; prior_sha256: string; prior_size: number } + | undefined; + if (snap) { + await mkdir(join(dest, dirname(script.target_rel_path)), { recursive: true }); + await writeFile(targetPath, snap.prior_content); + if (bss.prior_version_id) { + recordScript(bs.server, script.target_rel_path, bss.prior_version_id, snap.prior_sha256, snap.prior_size, `rollback:${betaId}`); } else { - const buf = await readFile(targetPath); - recordScript( - bs.server, - script.target_rel_path, - null, - sha256Buffer(buf), - buf.byteLength, - `rollback:${betaId}` - ); + recordScript(bs.server, script.target_rel_path, null, snap.prior_sha256, snap.prior_size, `rollback:${betaId}`); } restored.add(bs.server); } else { - warnings.push(`${bs.server}/${script.name}: no ${ofile} to restore`); + warnings.push(`${bs.server}/${script.name}: snapshot missing — cannot restore`); } } else { + // No prior file — the beta wrote a fresh file. Drop it. + if (existsSync(targetPath)) await rm(targetPath, { force: true }); + removeScript(bs.server, script.target_rel_path); dropped.add(bs.server); } } @@ -609,24 +604,17 @@ export async function rollbackBeta(betaId: number): Promise { if (hasCollapsing) { const dest = destOfKind(bs.server, 'config'); const targetRel = config.configTargetFile; - const backupPath = join(dest, oldFile(targetRel)); const targetPath = join(dest, targetRel); - if (existsSync(backupPath)) { - if (existsSync(targetPath)) await rm(targetPath, { force: true }); - await rename(backupPath, targetPath); - removeScript(bs.server, oldFile(targetRel)); - const buf = await readFile(targetPath); - recordScript( - bs.server, - targetRel, - null, - sha256Buffer(buf), - buf.byteLength, - `rollback:${betaId}` - ); + const snap = getSnapshot.get(betaId, bs.server, targetRel) as + | { prior_content: Buffer; prior_sha256: string; prior_size: number } + | undefined; + if (snap) { + await mkdir(dest, { recursive: true }); + await writeFile(targetPath, snap.prior_content); + recordScript(bs.server, targetRel, null, snap.prior_sha256, snap.prior_size, `rollback:${betaId}`); restored.add(bs.server); } else if (existsSync(targetPath)) { - // No .OLD ⇒ the beta wrote the cfg into a server that had + // No snapshot ⇒ the beta wrote the cfg into a server that had // no pre-existing file. Remove the beta-written cfg to leave // the server clean. await rm(targetPath, { force: true }); @@ -636,6 +624,9 @@ export async function rollbackBeta(betaId: number): Promise { } } + // Snapshots are single-use — safe to drop now that the disk is restored. + db().prepare('DELETE FROM beta_file_snapshots WHERE beta_id = ?').run(betaId); + // Wipe archive + close out beta. const archive = betaArchiveOf(betaId); if (existsSync(archive)) await rm(archive, { recursive: true, force: true }); @@ -722,6 +713,11 @@ export async function promoteBeta(betaId: number): Promise { } const plans: Plan[] = []; + const getSnapshotForPromote = db().prepare( + `SELECT prior_content, prior_sha256 FROM beta_file_snapshots + WHERE beta_id = ? AND server = ? AND rel_path = ?` + ); + for (const { script, version } of nonCollapsing) { const scopeServers = script.scope_type && script.scope_value ? serversMatchingScope(script.scope_type, script.scope_value) @@ -734,12 +730,14 @@ export async function promoteBeta(betaId: number): Promise { if (betaServerSet.has(name)) { const bss = getBetaServerScript(betaId, name, script.id); if (bss?.had_original) { - const backupPath = join(dest, oldFile(script.target_rel_path)); + const snap = getSnapshotForPromote.get(betaId, name, script.target_rel_path) as + | { prior_content: Buffer; prior_sha256: string } + | undefined; let priorContent: Buffer | null = null; let priorSha: string | null = null; - if (existsSync(backupPath)) { - priorContent = await readFile(backupPath); - priorSha = sha256Buffer(priorContent); + if (snap) { + priorContent = snap.prior_content; + priorSha = snap.prior_sha256; } else if (bss.prior_version_id) { const pv = getVersion(bss.prior_version_id); if (pv) { priorContent = pv.content; priorSha = pv.sha256; } @@ -777,15 +775,12 @@ export async function promoteBeta(betaId: number): Promise { const serverErr = new Map(); for (const p of plans) { const dest = destOfKind(p.server, p.script.kind); - const bfile = betaFile(p.script.target_rel_path); - const ofile = oldFile(p.script.target_rel_path); const targetPath = join(dest, p.script.target_rel_path); - const betaPath = join(dest, bfile); - const backupPath = join(dest, ofile); try { await mkdir(join(dest, dirname(p.script.target_rel_path)), { recursive: true }); - if (existsSync(betaPath)) { await rm(betaPath, { force: true }); removeScript(p.server, bfile); } - if (existsSync(backupPath)) { await rm(backupPath, { force: true }); removeScript(p.server, ofile); } + // Beta servers already have the version bytes at targetPath from + // openBeta; scope-expansion servers need the write. Idempotent + // either way — we just overwrite with the promoted content. await writeFile(targetPath, p.version.content); planWritten.set(p, true); } catch (err) { @@ -835,18 +830,24 @@ export async function promoteBeta(betaId: number): Promise { const dest = destOfKind(name, 'config'); const targetRel = config.configTargetFile; const targetPath = join(dest, targetRel); - const backupPath = join(dest, oldFile(targetRel)); let priorContent: Buffer | null = null; let priorSha: string | null = null; let hadFile = false; - // Beta server: prior was the pre-beta content saved as .OLD. - if (betaServerSet.has(name) && existsSync(backupPath)) { - priorContent = await readFile(backupPath); - priorSha = sha256Buffer(priorContent); - hadFile = true; - } else if (existsSync(targetPath)) { - // Scope-expansion server: current on-disk cfg is the "before". + // Beta server: prior cfg bytes were snapshotted at openBeta. + if (betaServerSet.has(name)) { + const snap = getSnapshotForPromote.get(betaId, name, targetRel) as + | { prior_content: Buffer; prior_sha256: string } + | undefined; + if (snap) { + priorContent = snap.prior_content; + priorSha = snap.prior_sha256; + hadFile = true; + } + } + if (!hadFile && existsSync(targetPath)) { + // Scope-expansion server (or beta server with no snapshot): + // current on-disk cfg is the "before". priorContent = await readFile(targetPath); priorSha = sha256Buffer(priorContent); hadFile = true; @@ -863,15 +864,9 @@ export async function promoteBeta(betaId: number): Promise { await mkdir(dest, { recursive: true }); const targetRel = config.configTargetFile; const targetPath = join(dest, targetRel); - const backupPath = join(dest, oldFile(targetRel)); // Beta server already has composed content at targetPath; overwrite - // idempotently. Scope-expansion server: write in place. Either way - // clear any lingering .OLD from the beta stage. + // idempotently. Scope-expansion server: write in place. await writeFile(targetPath, cp.composedContent); - if (existsSync(backupPath)) { - await rm(backupPath, { force: true }); - removeScript(cp.server, oldFile(targetRel)); - } cfgWritten.set(cp, true); } catch (err) { cfgWritten.set(cp, false); @@ -990,6 +985,9 @@ export async function promoteBeta(betaId: number): Promise { })(); db().prepare(`UPDATE betas SET state = 'promoted', closed_at = ? WHERE id = ?`).run(nowIso(), betaId); + // Snapshots are single-use — the promoted content is live on disk, so the + // pre-beta bytes are no longer needed. + db().prepare('DELETE FROM beta_file_snapshots WHERE beta_id = ?').run(betaId); const archive = betaArchiveOf(betaId); if (existsSync(archive)) await rm(archive, { recursive: true, force: true }); diff --git a/src/lib/server/fleet/drift.ts b/src/lib/server/fleet/drift.ts index 193761a..835317d 100644 --- a/src/lib/server/fleet/drift.ts +++ b/src/lib/server/fleet/drift.ts @@ -78,9 +78,11 @@ async function walkFiles(root: string): Promise { /** * Scan every managed destination tree on a server (scripts, configs, plugins) - * and return every file that isn't tracked in server_scripts. Beta artifacts - * (-beta.gsc, .OLD, -beta.dll, composed dedicated_sp.cfg) are tracked while - * a beta is active so they don't appear as drift. + * and return every file that isn't tracked in server_scripts. During a beta, + * the beta bytes live at the actual target filename and the inventory row is + * updated to reflect them, so beta files register as tracked with no + * special-casing (pre-beta bytes are snapshotted to beta_file_snapshots for + * rollback rather than left on disk as .OLD). * * The inventory table (server_scripts) keys on (server, rel_path) with no * destination discriminator. Rel_paths across destinations don't collide in diff --git a/src/lib/server/fleet/files.ts b/src/lib/server/fleet/files.ts index cf9dd76..f110afd 100644 --- a/src/lib/server/fleet/files.ts +++ b/src/lib/server/fleet/files.ts @@ -1,35 +1,5 @@ import { createHash } from 'node:crypto'; import { readFile, stat } from 'node:fs/promises'; -import { posix } from 'node:path'; - -// Naming rules for beta artifacts, preserved from the legacy shell script. - -/** - * `foo/bar.gsc` → `foo/bar-beta.gsc`. Used to name the beta copy that lives - * next to (or in place of) the target file on each server during a beta. - */ -export function betaFile(rel: string): string { - const dir = posix.dirname(rel); - const base = posix.basename(rel); - const dot = base.lastIndexOf('.'); - const stem = dot === -1 ? base : base.slice(0, dot); - const ext = dot === -1 ? '' : base.slice(dot); - const named = `${stem}-beta${ext}`; - return dir === '.' ? named : `${dir}/${named}`; -} - -/** - * `foo/bar.gsc` → `foo/bar.OLD`. Used to back up the previous target when a - * beta lands on top of it, so rollback can restore. - */ -export function oldFile(rel: string): string { - const dir = posix.dirname(rel); - const base = posix.basename(rel); - const dot = base.lastIndexOf('.'); - const stem = dot === -1 ? base : base.slice(0, dot); - const named = `${stem}.OLD`; - return dir === '.' ? named : `${dir}/${named}`; -} export async function sha256(path: string): Promise { const buf = await readFile(path); diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index a1da0f7..d6eaf3c 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -466,13 +466,13 @@
-

Scripts on this server ({inventory.filter((f) => !f.relPath.endsWith('.OLD') && !/-beta\./.test(f.relPath)).length})

+

Scripts on this server ({inventory.length})

{#if inventory.length === 0}

no scripts tracked — upload one from the Scripts page and open a beta to deploy it here

{:else}
{#each inventory as f (f.relPath)} -
+
{f.relPath} {f.size}B {f.source ?? '—'} @@ -910,9 +910,7 @@ font-size: var(--text-mono-sm); align-items: center; } - .inv-row.inv-old { opacity: 0.55; border-left: 3px solid var(--ink-500); } - .inv-row.inv-beta { border-left: 3px solid var(--amber-500); } - .inv-row.inv-live { border-left: 3px solid var(--spark-500); } + .inv-row { border-left: 3px solid var(--spark-500); } .inv-row .rel { color: var(--cream); word-break: break-all; } .inv-row .size, .inv-row .src, .inv-row .sha { color: var(--ink-500); } diff --git a/tests/unit/config-flow.test.ts b/tests/unit/config-flow.test.ts index f49aa36..3ae4bd9 100644 --- a/tests/unit/config-flow.test.ts +++ b/tests/unit/config-flow.test.ts @@ -34,7 +34,7 @@ async function seedFleet(): Promise { } 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); diff --git a/tests/unit/files.test.ts b/tests/unit/files.test.ts index b6fa43f..d0d29bc 100644 --- a/tests/unit/files.test.ts +++ b/tests/unit/files.test.ts @@ -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'); - }); -});