Files
electric-cherry/tests/unit/watchdog.test.ts
T
josh 981fb27c1c 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.
2026-09-27 21:48:55 -04:00

431 lines
26 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { newFixture, type Fixture } from '../helpers/fixture.js';
import { gameEvent } from '../helpers/telemetry.js';
vi.mock('../../src/lib/server/integrations/komodo.js', () => ({
komodoRestart: vi.fn(), komodoRuntimeAction: vi.fn(), komodoInspectHealthy: vi.fn(),
komodoCreateDeployment: vi.fn(), komodoDeploy: vi.fn(), komodoDestroyDeployment: vi.fn(), komodoDeleteDeployment: vi.fn(),
komodoEnsureTag: vi.fn(), komodoUpdateTagsOnResource: vi.fn()
}));
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({ rconGetPlayerCount: vi.fn(), rconStatusResult: vi.fn() }));
vi.mock('../../src/lib/server/pipeline/worker.js', () => ({ kick: vi.fn(async () => {}) }));
const server = 't5_zombie_theater_1';
const healthyRcon = { ok: true as const, endpoint: { host: 'test', port: 28960 }, rawReply: '',
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } };
const missingRcon = { ok: false as const, endpoint: { host: 'test', port: 28960 }, reason: 'timeout' as const, detail: 'No reply' };
let fixture: Fixture;
type Engine = typeof import('../../src/lib/server/watchdog/index.js');
let watchdog: Engine;
let database: typeof import('../../src/lib/server/db/index.js');
let komodo: typeof import('../../src/lib/server/integrations/komodo.js');
let rcon: typeof import('../../src/lib/server/integrations/rcon.js');
beforeEach(async () => {
vi.resetModules(); vi.resetAllMocks();
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-27T12:00:00Z'));
fixture = await newFixture([server]);
for (const [key, value] of Object.entries({ KOMODO_ADDRESS: 'http://komodo.test', KOMODO_API_KEY: 'test', KOMODO_API_SECRET: 'test',
KOMODO_TARGET_SERVER: 'host', RCON_PASSWORD: 'test', DEPLOYMENT_CREATE_CONCURRENCY: '1', DEPLOYMENT_RESTART_CONCURRENCY: '1',
SERVER_PORT_MIN: '28960', SERVER_PORT_MAX: '28970' })) vi.stubEnv(key, value);
database = await import('../../src/lib/server/db/index.js');
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
await seedServerRegistry();
database.db().prepare('UPDATE servers SET game_port = 28960 WHERE name = ?').run(server);
const { saveServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
saveServerKey(server, 'saved-key');
komodo = await import('../../src/lib/server/integrations/komodo.js');
for (const name of ['komodoRestart', 'komodoRuntimeAction', 'komodoCreateDeployment', 'komodoDeploy', 'komodoDestroyDeployment', 'komodoDeleteDeployment'] as const) {
vi.mocked(komodo[name]).mockResolvedValue({ ok: true });
}
vi.mocked(komodo.komodoEnsureTag).mockImplementation(async name => ({ ok: true, id: name }));
vi.mocked(komodo.komodoUpdateTagsOnResource).mockResolvedValue({ ok: true });
vi.mocked(komodo.komodoInspectHealthy).mockResolvedValue({ ok: true, state: 'running' });
rcon = await import('../../src/lib/server/integrations/rcon.js');
vi.mocked(rcon.rconGetPlayerCount).mockResolvedValue(0);
vi.mocked(rcon.rconStatusResult).mockResolvedValue(healthyRcon);
watchdog = await import('../../src/lib/server/watchdog/index.js');
});
afterEach(async () => { await fixture.cleanup(); vi.useRealTimers(); vi.unstubAllEnvs(); });
const incidents = () => watchdog.watchdogOverview().incidents;
const onlyIncident = () => { expect(incidents()).toHaveLength(1); return incidents()[0]; };
const operations = () => database.db().prepare('SELECT id, action, state FROM server_deployments ORDER BY rowid').all() as { id: string; action: string; state: string }[];
function advance(ms: number) { vi.setSystemTime(Date.now() + ms); }
function down() { vi.mocked(komodo.komodoInspectHealthy).mockResolvedValue({ ok: false, state: 'exited', reason: 'Container exited' }); }
async function sustainedFailure() {
down();
await watchdog.watchdogTick(true);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(operations()).toEqual([expect.objectContaining({ action: 'restart', state: 'waiting' })]);
return onlyIncident();
}
describe('Watchdog detection and ownership', () => {
it('defaults to automatic recovery and debounces manual scans without accelerating the failure window', async () => {
expect(watchdog.watchdogOverview().enabled).toBe(true);
down();
await watchdog.watchdogTick(true);
expect(onlyIncident()).toMatchObject({ state: 'open', failures: 1 });
for (let n = 0; n < 5; n++) await watchdog.watchdogTick(true);
expect(onlyIncident().failures).toBe(1);
expect(operations()).toEqual([]);
advance(watchdog.WATCHDOG_SCAN_MS);
await watchdog.watchdogTick();
expect(onlyIncident().failures).toBe(2);
advance(watchdog.WATCHDOG_SCAN_MS - 1);
await watchdog.watchdogTick(true);
expect(operations()).toEqual([]);
advance(1);
await watchdog.watchdogTick(true);
const row = onlyIncident();
expect(row).toMatchObject({ state: 'recovering', failures: 3 });
expect(watchdog.watchdogDetail(row.id)?.actions).toEqual([expect.objectContaining({ action: 'restart', operation_state: 'waiting' })]);
await watchdog.watchdogTick(true);
expect(operations()).toHaveLength(1);
expect(komodo.komodoRestart).not.toHaveBeenCalled();
});
it('keeps paused recovery observational and resumes one pending incident', async () => {
watchdog.setWatchdogEnabled(false);
down();
for (let n = 0; n < 4; n++) { await watchdog.watchdogTick(true); advance(watchdog.WATCHDOG_SCAN_MS); }
expect(onlyIncident()).toMatchObject({ state: 'open', failures: 4 });
expect(operations()).toEqual([]);
database.closeDb();
expect(watchdog.watchdogOverview().enabled).toBe(false);
watchdog.setWatchdogEnabled(true);
await watchdog.watchdogTick(true);
expect(operations()).toEqual([expect.objectContaining({ action: 'restart' })]);
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
watchdog.setWatchdogEnabled(false);
await processProvisioning();
expect(operations()[0].state).toBe('waiting');
expect(komodo.komodoRestart).not.toHaveBeenCalled();
watchdog.setWatchdogEnabled(true);
await processProvisioning();
expect(komodo.komodoRestart).toHaveBeenCalledTimes(1);
});
it('does not restart an unknown Komodo state even after repeated sustained observations', async () => {
vi.mocked(komodo.komodoInspectHealthy).mockResolvedValue({ ok: false, reason: 'Komodo request timed out' });
for (let n = 0; n < 8; n++) { await watchdog.watchdogTick(true); advance(watchdog.WATCHDOG_SCAN_MS); }
expect(onlyIncident()).toMatchObject({ kind: 'health', state: 'open' });
expect(operations()).toEqual([]);
expect(rcon.rconStatusResult).not.toHaveBeenCalled();
down();
await watchdog.watchdogTick(true);
expect(onlyIncident().failures).toBe(1);
expect(operations()).toEqual([]);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(operations()).toEqual([]);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(operations()).toEqual([expect.objectContaining({ action: 'restart' })]);
});
it('allows a longer initial startup grace period before requesting recovery', async () => {
vi.mocked(komodo.komodoInspectHealthy).mockResolvedValue({ ok: false, state: 'starting', reason: 'Starting' });
for (let n = 0; n < 5; n++) { await watchdog.watchdogTick(true); advance(watchdog.WATCHDOG_SCAN_MS); }
expect(operations()).toEqual([]);
await watchdog.watchdogTick(true);
expect(operations()).toEqual([expect.objectContaining({ action: 'restart' })]);
});
it('accepts fresh validated telemetry when RCON is unavailable and rechecks after it becomes stale', async () => {
const { saveTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
vi.mocked(rcon.rconStatusResult).mockResolvedValue(missingRcon);
saveTelemetry({ ...gameEvent('heartbeat', 17, 4, server), receivedAt: new Date().toISOString() });
await watchdog.watchdogTick(true);
expect(incidents()).toEqual([]);
expect(rcon.rconStatusResult).not.toHaveBeenCalled();
advance(60_000);
await watchdog.watchdogTick(true);
expect(onlyIncident()).toMatchObject({ kind: 'health', state: 'open', failures: 1 });
saveTelemetry({ ...gameEvent('heartbeat', 18, 4, server), receivedAt: new Date().toISOString() });
await watchdog.watchdogTick(true);
expect(onlyIncident().state).toBe('resolved');
expect(operations()).toEqual([]);
});
it('preserves intentional stop intent after completion and database reopening', async () => {
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
const stop = requestLifecycle(server, 'stop');
expect(stop.ok).toBe(true);
if (!stop.ok) throw new Error(stop.error);
database.db().prepare("UPDATE server_deployments SET state = 'done', finished_at = ? WHERE id = ?").run(new Date().toISOString(), stop.deploymentId);
down();
database.closeDb();
for (let n = 0; n < 4; n++) { await watchdog.watchdogTick(true); advance(watchdog.WATCHDOG_SCAN_MS); }
expect(incidents()).toEqual([]);
expect(operations()).toEqual([expect.objectContaining({ action: 'stop' })]);
expect(komodo.komodoInspectHealthy).not.toHaveBeenCalled();
});
it.each(['beta', 'campaign'] as const)('defers a server owned by %s work', async (owner) => {
if (owner === 'beta') {
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
const { script, version } = createScript({ name: 'beta', content: Buffer.from('experimental'), targetRelPath: 'beta.gsc' });
await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }], servers: [server], waitForHealth: false, waitForMatch: false });
} else {
database.db().prepare("INSERT INTO deployments (id, action, started_at) VALUES ('campaign', 'promote', ?)").run(new Date().toISOString());
database.db().prepare("INSERT INTO deployment_servers (deployment_id, server, files_changed, restart_state) VALUES ('campaign', ?, 1, 'held')").run(server);
}
down();
for (let n = 0; n < 4; n++) { await watchdog.watchdogTick(true); advance(watchdog.WATCHDOG_SCAN_MS); }
expect(incidents()).toEqual([]);
expect(operations()).toEqual([]);
expect(komodo.komodoInspectHealthy).not.toHaveBeenCalled();
});
it('rechecks lifecycle ownership after a health probe yields to an operator stop', async () => {
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
vi.mocked(komodo.komodoInspectHealthy).mockImplementationOnce(async () => {
expect(requestLifecycle(server, 'stop').ok).toBe(true);
return { ok: false, state: 'exited', reason: 'Container exited' };
});
await watchdog.watchdogTick(true);
expect(incidents()).toEqual([]);
expect(operations()).toEqual([expect.objectContaining({ action: 'stop' })]);
});
});
describe('Watchdog recovery persistence', () => {
it('preserves the original finding as deleted history after an intentional successful destroy', async () => {
const { removeServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
const { destroyServer, processDestructions } = await import('../../src/lib/server/provisioning/instances.js');
removeServerKey(server);
await writeFile(join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc'), 'unexpected');
await watchdog.watchdogTick(true);
const id = onlyIncident().id;
expect(onlyIncident().state).toBe('attention');
expect(destroyServer(server)).toMatchObject({ ok: true });
down();
await processDestructions();
expect(operations()).toEqual([expect.objectContaining({ action: 'destroy', state: 'done' })]);
expect(database.db().prepare('SELECT name FROM servers WHERE name = ?').get(server)).toBeUndefined();
await watchdog.watchdogTick(true);
expect(watchdog.watchdogDetail(id)?.incident).toMatchObject({ state: 'attention', server_deleted_at: expect.any(String) });
expect(watchdog.updateWatchdogIncident(id, 'retry')).toMatchObject({ ok: false, status: 409 });
});
it('requests one restart, escalates once to rebuild, and retains exhausted attention across reopening', async () => {
const row = await sustainedFailure();
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
await processProvisioning();
expect(komodo.komodoRestart).toHaveBeenCalledTimes(1);
expect(operations()[0].state).toBe('awaiting-health');
advance(watchdog.WATCHDOG_RECOVERY_MS - 1);
await watchdog.watchdogTick(true);
expect(operations()).toHaveLength(1);
advance(1);
await watchdog.watchdogTick(true);
expect(operations().map(op => op.action)).toEqual(['restart', 'rebuild']);
expect(watchdog.watchdogDetail(row.id)?.actions).toHaveLength(2);
vi.mocked(komodo.komodoDestroyDeployment).mockResolvedValue({ ok: false, kind: 'unreachable', message: 'Host unavailable' });
await processProvisioning();
expect(operations()[1].state).toBe('failed');
await watchdog.watchdogTick(true);
expect(watchdog.watchdogDetail(row.id)?.incident.state).toBe('attention');
database.closeDb();
for (let n = 0; n < 4; n++) { advance(watchdog.WATCHDOG_RECOVERY_MS); await watchdog.watchdogTick(true); }
expect(operations().map(op => op.action)).toEqual(['restart', 'rebuild']);
expect(watchdog.watchdogDetail(row.id)?.incident.state).toBe('attention');
expect(komodo.komodoRestart).toHaveBeenCalledTimes(1);
expect(komodo.komodoDestroyDeployment).toHaveBeenCalledTimes(1);
});
it('does not escalate a timed out restart while automatic recovery is paused', async () => {
await sustainedFailure();
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
await processProvisioning();
watchdog.setWatchdogEnabled(false);
advance(watchdog.WATCHDOG_RECOVERY_MS);
await watchdog.watchdogTick(true);
expect(operations()).toHaveLength(1);
watchdog.setWatchdogEnabled(true);
await watchdog.watchdogTick(true);
expect(operations().map(op => op.action)).toEqual(['restart', 'rebuild']);
});
it('resumes a persisted detection window and does not duplicate queued work after reopening', async () => {
down();
await watchdog.watchdogTick(true);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
const id = onlyIncident().id;
database.closeDb();
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(onlyIncident().id).toBe(id);
const queued = operations();
expect(queued).toHaveLength(1);
database.closeDb();
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(operations()).toEqual(queued);
expect(watchdog.watchdogDetail(id)?.actions).toHaveLength(1);
});
it('permits an explicit new attempt after dismissal without silently rearming a sustained condition', async () => {
down();
await watchdog.watchdogTick(true);
const id = onlyIncident().id;
expect(watchdog.updateWatchdogIncident(id, 'dismiss')).toEqual({ ok: true });
for (let n = 0; n < 4; n++) { advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true); }
expect(operations()).toEqual([]);
expect(onlyIncident().state).toBe('dismissed');
expect(watchdog.updateWatchdogIncident(id, 'retry')).toEqual({ ok: true });
await watchdog.watchdogTick(true);
expect(operations()).toEqual([]);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(onlyIncident()).toMatchObject({ id, state: 'recovering', cycle: 2 });
expect(watchdog.watchdogDetail(id)?.actions).toEqual([expect.objectContaining({ action: 'restart', cycle: 2 })]);
});
});
describe('Watchdog APIs', () => {
const request = (body: unknown, raw = false) => new Request('http://localhost/api/watchdog', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: raw ? String(body) : JSON.stringify(body)
});
it('returns overview state and validates scan actions and pause booleans', async () => {
const { GET, POST } = await import('../../src/routes/api/watchdog/+server.js');
expect(await GET().json()).toEqual({ enabled: true, lastScanAt: null, scanError: null, incidents: [] });
expect((await POST({ request: request('{', true) })).status).toBe(400);
for (const body of [null, [], {}, { action: 'restart' }, { enabled: 'false' }, { enabled: 0 }, { action: 'scan', enabled: 'false' }]) {
expect((await POST({ request: request(body) })).status).toBe(400);
}
expect(await (await POST({ request: request({ enabled: false }) })).json()).toMatchObject({ enabled: false });
database.closeDb();
expect(await GET().json()).toMatchObject({ enabled: false });
down();
const scanned = await POST({ request: request({ action: 'scan' }) });
expect(scanned.status).toBe(200);
expect(await scanned.json()).toMatchObject({ enabled: false, lastScanAt: expect.any(String), incidents: [expect.objectContaining({ kind: 'health', state: 'open' })] });
expect(operations()).toEqual([]);
});
it('validates incident actions and refuses retries while provisioning is pending', async () => {
const { GET, POST } = await import('../../src/routes/api/watchdog/[id]/+server.js');
expect(GET({ params: { id: 'missing' } }).status).toBe(404);
expect((await POST({ params: { id: 'missing' }, request: request({ action: 'retry' }) })).status).toBe(404);
for (const body of [null, {}, [], { action: 'restart' }]) {
expect((await POST({ params: { id: 'missing' }, request: request(body) })).status).toBe(400);
}
expect((await POST({ params: { id: 'missing' }, request: request('{', true) })).status).toBe(400);
const row = await sustainedFailure();
expect(await GET({ params: { id: row.id } }).json()).toMatchObject({
incident: { id: row.id, state: 'recovering' }, actions: [expect.objectContaining({ action: 'restart' })], events: expect.any(Array)
});
for (const action of ['retry', 'dismiss']) {
const response = await POST({ params: { id: row.id }, request: request({ action }) });
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({ error: expect.stringContaining('Provisioning is still active') });
}
expect(operations()).toHaveLength(1);
});
it('keeps legacy drift previews readable and directs mutations to Watchdog without changing files', async () => {
const { GET, POST } = await import('../../src/routes/api/drift/+server.js');
const path = join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc');
await writeFile(path, 'unexpected content');
expect(await (await GET()).json()).toMatchObject({ totalDrift: 1, servers: [{ server, files: [expect.objectContaining({ relPath: 'stray.gsc' })] }] });
const preview = await POST({ request: request({ dryRun: true, servers: [server] }) });
expect(preview.status).toBe(200);
const body = await preview.json();
expect(body).toMatchObject({ ok: true, plan: { servers: [server], fingerprint: expect.any(String), files: [expect.objectContaining({ relPath: 'stray.gsc' })] } });
const mutation = await POST({ request: request({ servers: [server], expectedFingerprint: body.plan.fingerprint }) });
expect(mutation.status).toBe(410);
expect(await mutation.json()).toMatchObject({ ok: false, watchdogUrl: '/watchdog', error: expect.stringContaining('Watchdog') });
expect(await readFile(path, 'utf8')).toBe('unexpected content');
expect(operations()).toEqual([]);
expect(database.db().prepare('SELECT id FROM deployments').all()).toEqual([]);
});
});
describe('Watchdog drift campaigns', () => {
it('queues an audited rebuild without changing files until provisioning executes and retains its campaign through cleanup', async () => {
await mkdir(join(fixture.t5, server, 'configs'), { recursive: true });
const path = join(fixture.t5, server, 'configs', 'stray.cfg');
await writeFile(path, 'unexpected content');
await watchdog.watchdogTick(true);
const row = onlyIncident();
expect(row).toMatchObject({ kind: 'drift', state: 'recovering' });
const action = watchdog.watchdogDetail(row.id)!.actions[0];
expect(action).toMatchObject({ action: 'rebuild', campaign_id: expect.any(String), operation_state: 'waiting' });
expect(await readFile(path, 'utf8')).toBe('unexpected content');
expect(komodo.komodoDestroyDeployment).not.toHaveBeenCalled();
expect(database.db().prepare('SELECT rel_path, content_before, content_after FROM deployment_files WHERE deployment_id = ?').all(action.campaign_id))
.toEqual([{ rel_path: 'stray.cfg', content_before: Buffer.from('unexpected content'), content_after: null }]);
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
const { processCreations } = await import('../../src/lib/server/provisioning/instances.js');
const { sweep } = await import('../../src/lib/server/pipeline/sweep.js');
down();
await processProvisioning();
expect(operations()).toEqual([expect.objectContaining({ action: 'rebuild', state: 'queued' })]);
await expect(access(path)).rejects.toThrow();
expect(database.db().prepare('SELECT name FROM servers WHERE name = ?').get(server)).toBeUndefined();
database.closeDb();
await sweep(); await watchdog.watchdogTick(true);
expect(watchdog.watchdogDetail(row.id)?.incident.state).toBe('recovering');
expect(database.db().prepare('SELECT provisioning_id, restart_state FROM deployment_servers WHERE deployment_id = ?').get(action.campaign_id))
.toEqual({ provisioning_id: action.operation_id, restart_state: 'pending' });
await processCreations();
vi.mocked(komodo.komodoInspectHealthy).mockResolvedValue({ ok: true, state: 'running' });
await processCreations(); await sweep(); await watchdog.watchdogTick(true);
expect(watchdog.watchdogDetail(row.id)?.incident.state).toBe('resolved');
expect(database.db().prepare('SELECT restart_state FROM deployment_servers WHERE deployment_id = ?').get(action.campaign_id)).toEqual({ restart_state: 'done' });
expect(operations()).toEqual([expect.objectContaining({ action: 'rebuild', state: 'done' })]);
expect(komodo.komodoRestart).not.toHaveBeenCalled();
});
it('keeps a missing rebuild key visible as attention without deleting files or retrying automatically', async () => {
const { removeServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
removeServerKey(server);
const path = join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc');
await writeFile(path, 'unexpected');
await watchdog.watchdogTick(true);
expect(onlyIncident()).toMatchObject({ kind: 'drift', state: 'attention', summary: expect.stringContaining('Save a server key') });
advance(watchdog.WATCHDOG_SCAN_MS); await watchdog.watchdogTick(true);
expect(operations()).toEqual([]);
expect(await readFile(path, 'utf8')).toBe('unexpected');
expect(komodo.komodoDestroyDeployment).not.toHaveBeenCalled();
});
});
describe('Maintenance recovery policy boundaries', () => {
it('separates health recovery from drift repair and holds an already queued drift rebuild', async () => {
const policies = await import('../../src/lib/server/maintenance/policies.js');
policies.setMaintenancePolicy('health', false);
down();
for (let n = 0; n < 3; n++) { await watchdog.watchdogTick(true); advance(watchdog.WATCHDOG_SCAN_MS); }
expect(operations()).toEqual([]);
expect(onlyIncident().kind).toBe('health');
vi.mocked(komodo.komodoInspectHealthy).mockResolvedValue({ ok: true, state: 'running' });
await writeFile(join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc'), 'drift');
await watchdog.watchdogTick(true);
expect(operations()).toEqual([expect.objectContaining({ action: 'rebuild', state: 'waiting' })]);
policies.setMaintenancePolicy('drift', false);
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
await processProvisioning();
expect(operations()[0].state).toBe('waiting');
expect(komodo.komodoDestroyDeployment).not.toHaveBeenCalled();
});
it('global pause blocks escalation without disabling the saved health policy', async () => {
await sustainedFailure();
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
await processProvisioning();
const policies = await import('../../src/lib/server/maintenance/policies.js');
policies.pauseMaintenance(true);
advance(watchdog.WATCHDOG_RECOVERY_MS); await watchdog.watchdogTick(true);
expect(operations()).toHaveLength(1);
expect(policies.maintenancePolicies().health).toBe(true);
policies.pauseMaintenance(false); await watchdog.watchdogTick(true);
expect(operations()).toHaveLength(2);
expect(operations()[1].action).toBe('rebuild');
});
});