Hide live capacity for inactive provisioning operations (Refs #85)
This commit is contained in:
@@ -95,8 +95,9 @@ function sourceDetails(source: Omit<ProvisioningSource, 'label' | 'href'>): Prov
|
|||||||
href: row ? `/maintenance?view=activity#job-${encodeURIComponent(id)}` : null };
|
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 {
|
function capacityFor(deployment: ProvisioningDeployment, evaluatedAt: string): ProvisioningCapacity {
|
||||||
|
if (!['queued', 'waiting', 'running', 'awaiting-health'].includes(deployment.state)) return { evaluatedAt, pools: [] };
|
||||||
const settings = fleetSettings();
|
const settings = fleetSettings();
|
||||||
const pools: ProvisioningCapacity['pools'] = [];
|
const pools: ProvisioningCapacity['pools'] = [];
|
||||||
if (deployment.action === 'create' || deployment.action === 'rebuild') {
|
if (deployment.action === 'create' || deployment.action === 'rebuild') {
|
||||||
|
|||||||
@@ -70,21 +70,29 @@ describe('provisioning detail projection', () => {
|
|||||||
expect(getDeployment('legacy')!.warningMessages).toEqual(['Recorded warnings could not be read.']);
|
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 { db } = await import('../../src/lib/server/db/index.js');
|
||||||
const { beginDeployment, getDeployment, finishDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
const { beginDeployment, getDeployment, finishDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||||
db().prepare("INSERT INTO servers (name) VALUES ('server')").run();
|
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' });
|
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);
|
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,
|
const kinds = action === 'create' ? ['startup'] : action === 'rebuild' ? ['startup', 'restart'] : ['restart'];
|
||||||
holders: [{ id: current, server: 'server' }] });
|
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(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());
|
db().prepare('UPDATE restart_rollout SET healthy_at = ?').run(new Date().toISOString());
|
||||||
expect(getDeployment(old)!.capacity.pools[0].used).toBe(0);
|
expect(getDeployment(old)!.capacity.pools).toEqual([]);
|
||||||
expect(getDeployment(old)!.deployment.state).toBe('failed');
|
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 () => {
|
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' });
|
history.finishDeployment(id, { ok: false, message: 'Observation interrupted' });
|
||||||
const failed = history.getDeployment(id)!;
|
const failed = history.getDeployment(id)!;
|
||||||
expect(failed.progress?.phase).toBe('verify-game');
|
expect(failed.progress?.phase).toBe('verify-game');
|
||||||
|
expect(failed.capacity.pools).toEqual([]);
|
||||||
expect(history.resumeVerification(id)).toBe(true);
|
expect(history.resumeVerification(id)).toBe(true);
|
||||||
const resumed = history.getDeployment(id)!;
|
const resumed = history.getDeployment(id)!;
|
||||||
expect(resumed.deployment).toMatchObject({ id, state: 'awaiting-health', finished_at: null, error: null });
|
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.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');
|
const outcomes = resumed.events.filter(event => event.kind === 'outcome');
|
||||||
expect(outcomes).toHaveLength(1);
|
expect(outcomes).toHaveLength(1);
|
||||||
expect(JSON.parse(outcomes[0].details_json!)).toMatchObject({ state: 'failed', finishedAt: failed.deployment.finished_at,
|
expect(JSON.parse(outcomes[0].details_json!)).toMatchObject({ state: 'failed', finishedAt: failed.deployment.finished_at,
|
||||||
|
|||||||
Reference in New Issue
Block a user