Preserve deleted server history
Retain historical server identities, results, and evidence after deletion. Mark archived entries as Deleted and prevent old work from targeting servers that reuse the same name. Fixes #16.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
const server = 't5_zombie_theater_1';
|
||||
const at = '2026-09-27T12:00:00.000Z';
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([server]);
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
});
|
||||
afterEach(async () => fixture.cleanup());
|
||||
|
||||
it('exposes retained campaign and provisioning history without rebinding links to a reused name', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
const { beginDeployment, finishDeployment, getDeployment, listDeployments } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const operation = beginDeployment(server, 'restart');
|
||||
finishDeployment(operation, { ok: true });
|
||||
db().prepare("INSERT INTO deployments (id, action, started_at, finished_at) VALUES ('history-campaign', 'promote', ?, ?)").run(at, at);
|
||||
db().prepare("INSERT INTO deployment_servers (deployment_id, server, files_changed, restart_state) VALUES ('history-campaign', ?, 1, 'done')").run(server);
|
||||
db().prepare("INSERT INTO deployment_files (deployment_id, server, verb, rel_path, content_before, content_after) VALUES ('history-campaign', ?, 'updated', 'script.gsc', ?, ?)")
|
||||
.run(server, Buffer.from('before'), Buffer.from('after'));
|
||||
db().prepare('DELETE FROM servers WHERE name = ?').run(server);
|
||||
await seedServerRegistry();
|
||||
closeDb();
|
||||
const { GET: campaignGET } = await import('../../src/routes/api/campaigns/[id]/+server.js');
|
||||
const detail = await (await campaignGET({ params: { id: 'history-campaign' } })).json();
|
||||
expect(detail.servers).toEqual([expect.objectContaining({ server, restart_state: 'done', server_deleted_at: expect.any(String) })]);
|
||||
expect(detail.files).toEqual([expect.objectContaining({ server, rel_path: 'script.gsc', size_before: 6, size_after: 5 })]);
|
||||
const { GET: listGET } = await import('../../src/routes/api/campaigns/+server.js');
|
||||
const list = await (await listGET({ url: new URL('http://localhost/api/campaigns') })).json();
|
||||
expect(list.deployments[0].servers[0].server_deleted_at).toEqual(expect.any(String));
|
||||
expect(getDeployment(operation)?.deployment).toMatchObject({ server, state: 'done', server_exists: 0, server_deleted_at: expect.any(String) });
|
||||
expect(listDeployments()).toContainEqual(expect.objectContaining({ id: operation, server_exists: 0 }));
|
||||
const replacement = beginDeployment(server, 'restart');
|
||||
expect(getDeployment(replacement)?.deployment).toMatchObject({ server_exists: 1, server_deleted_at: null });
|
||||
});
|
||||
|
||||
it('rejects historical maintenance retries and allows a new incident for a reused server name', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
const job = db().prepare("INSERT INTO maintenance_jobs (server, requested_at, state, detail) VALUES (?, ?, 'failed', 'Original result')").run(server, at).lastInsertRowid;
|
||||
db().prepare("INSERT INTO watchdog_incidents (id, server, kind, state, summary, first_seen_at, last_seen_at) VALUES ('old-incident', ?, 'health', 'attention', 'Original finding', ?, ?)")
|
||||
.run(server, at, at);
|
||||
db().prepare('DELETE FROM servers WHERE name = ?').run(server);
|
||||
await seedServerRegistry();
|
||||
const { retryMaintenance, plutoniumStatus } = await import('../../src/lib/server/maintenance/runtime.js');
|
||||
const { updateWatchdogIncident, watchdogDetail } = await import('../../src/lib/server/maintenance/recovery.js');
|
||||
expect(() => retryMaintenance(Number(job))).toThrow('deleted');
|
||||
expect(plutoniumStatus().jobs).toContainEqual(expect.objectContaining({ id: Number(job), state: 'failed', detail: 'Original result', server_deleted_at: expect.any(String) }));
|
||||
expect(updateWatchdogIncident('old-incident', 'retry')).toMatchObject({ ok: false, status: 409 });
|
||||
expect(watchdogDetail('old-incident')?.incident).toMatchObject({ state: 'attention', summary: 'Original finding', server_deleted_at: expect.any(String) });
|
||||
expect(() => db().prepare("INSERT INTO watchdog_incidents (id, server, kind, state, summary, first_seen_at, last_seen_at) VALUES ('new-incident', ?, 'health', 'open', 'New finding', ?, ?)")
|
||||
.run(server, at, at)).not.toThrow();
|
||||
});
|
||||
Reference in New Issue
Block a user