From cdc400c6bbc7c9103fdd5b3d102cc07aaf138e7f Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Fri, 9 Oct 2026 09:55:36 -0400 Subject: [PATCH] Hide live capacity for inactive provisioning operations (Refs #85) --- src/lib/server/provisioning/history.ts | 3 ++- tests/unit/provisioning-detail.test.ts | 26 ++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/lib/server/provisioning/history.ts b/src/lib/server/provisioning/history.ts index 461987f..f480297 100644 --- a/src/lib/server/provisioning/history.ts +++ b/src/lib/server/provisioning/history.ts @@ -95,8 +95,9 @@ function sourceDetails(source: Omit): Prov href: row ? `/maintenance?view=activity#job-${encodeURIComponent(id)}` : null }; } -/** These are current pool snapshots, not a second admission/eligibility engine. */ +/** Active operations get current pool snapshots, not a second admission/eligibility engine. */ function capacityFor(deployment: ProvisioningDeployment, evaluatedAt: string): ProvisioningCapacity { + if (!['queued', 'waiting', 'running', 'awaiting-health'].includes(deployment.state)) return { evaluatedAt, pools: [] }; const settings = fleetSettings(); const pools: ProvisioningCapacity['pools'] = []; if (deployment.action === 'create' || deployment.action === 'rebuild') { diff --git a/tests/unit/provisioning-detail.test.ts b/tests/unit/provisioning-detail.test.ts index 9ceb403..e9646d8 100644 --- a/tests/unit/provisioning-detail.test.ts +++ b/tests/unit/provisioning-detail.test.ts @@ -70,21 +70,29 @@ describe('provisioning detail projection', () => { expect(getDeployment('legacy')!.warningMessages).toEqual(['Recorded warnings could not be read.']); }); - it('correlates reservations by operation ID without assigning a later same-name slot to historical work', async () => { + it.each(['create', 'rebuild', 'start', 'restart'] as const)('shows live capacity only for active %s operations, excluding historical work on the same server', async action => { const { db } = await import('../../src/lib/server/db/index.js'); const { beginDeployment, getDeployment, finishDeployment } = await import('../../src/lib/server/provisioning/history.js'); db().prepare("INSERT INTO servers (name) VALUES ('server')").run(); - const old = beginDeployment('server', 'restart'); + const old = beginDeployment('server', action); finishDeployment(old, { ok: false, message: 'Timeout' }); - const current = beginDeployment('server', 'restart'); + const current = beginDeployment('server', action); db().prepare("INSERT INTO restart_rollout (server,requested_at,operation_id) VALUES ('server','2026-01-01',?)").run(current); - expect(getDeployment(old)!.capacity.pools[0]).toMatchObject({ kind: 'restart', used: 1, ownsSlot: false, - holders: [{ id: current, server: 'server' }] }); + const kinds = action === 'create' ? ['startup'] : action === 'rebuild' ? ['startup', 'restart'] : ['restart']; + expect(getDeployment(current)!.capacity.pools).toEqual(kinds.map(kind => expect.objectContaining({ + kind, used: 1, ownsSlot: true, holders: [{ id: current, server: 'server' }] + }))); + for (const state of ['done', 'warning', 'failed', 'interrupted', 'cancelled']) { + db().prepare('UPDATE server_deployments SET state = ? WHERE id = ?').run(state, old); + expect(getDeployment(old)!.capacity.pools, state).toEqual([]); + } expect(getDeployment(old)!.progress!.recovery).toBeNull(); - expect(getDeployment(current)!.capacity.pools[0].ownsSlot).toBe(true); db().prepare('UPDATE restart_rollout SET healthy_at = ?').run(new Date().toISOString()); - expect(getDeployment(old)!.capacity.pools[0].used).toBe(0); - expect(getDeployment(old)!.deployment.state).toBe('failed'); + expect(getDeployment(old)!.capacity.pools).toEqual([]); + for (const state of ['queued', 'waiting', 'running', 'awaiting-health']) { + db().prepare('UPDATE server_deployments SET state = ? WHERE id = ?').run(state, current); + expect(getDeployment(current)!.capacity.pools.map(pool => pool.kind), state).toEqual(kinds); + } }); it('labels submission order independently of startup reservations and retains queued recipe status', async () => { @@ -113,10 +121,12 @@ describe('provisioning detail projection', () => { history.finishDeployment(id, { ok: false, message: 'Observation interrupted' }); const failed = history.getDeployment(id)!; expect(failed.progress?.phase).toBe('verify-game'); + expect(failed.capacity.pools).toEqual([]); expect(history.resumeVerification(id)).toBe(true); const resumed = history.getDeployment(id)!; expect(resumed.deployment).toMatchObject({ id, state: 'awaiting-health', finished_at: null, error: null }); expect(resumed.progress).toMatchObject({ phase: 'verify-container', checks: [], observedAt: null }); + expect(resumed.capacity.pools).toEqual([expect.objectContaining({ kind: 'startup', ownsSlot: true })]); const outcomes = resumed.events.filter(event => event.kind === 'outcome'); expect(outcomes).toHaveLength(1); expect(JSON.parse(outcomes[0].details_json!)).toMatchObject({ state: 'failed', finishedAt: failed.deployment.finished_at,