Fix failed provisioning progress states (Refs #80)

This commit is contained in:
2026-10-09 10:21:02 -04:00
parent dbbe3749e2
commit 29c2e3ba00
3 changed files with 69 additions and 7 deletions
@@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest';
import { render } from 'svelte/server';
import type { ProvisioningDetail, ProvisioningProgress } from '../../src/lib/provisioning-detail.js';
import { provisioningActions } from '../../src/lib/provisioning.js';
import { isProvisioningDetail, isProvisioningEventPage, provisioningGroups, provisioningOutcome, provisioningRequester, provisioningReturn } from '../../src/lib/ui/provisioning-presentation.js';
import ProvisioningProgressView from '../../src/lib/ui/components/ProvisioningProgress.svelte';
const at = '2026-09-28T12:00:00Z';
function progress(patch: Partial<ProvisioningProgress> = {}): ProvisioningProgress {
@@ -119,6 +121,62 @@ describe('provisioning outcome and evidence presentation', () => {
expect(provisioningGroups(detail).flatMap(group => group.steps).find(step => step.key === 'verify-game')).toMatchObject({ state: 'unknown', at: '2026-09-28T12:01:00Z' });
});
it.each(['failed', 'interrupted'])('shows where an unhealthy container stopped a %s operation', state => {
const detail = fixture();
detail.deployment.state = state;
detail.deployment.finished_at = at;
detail.progress = progress({ phase: 'verify-container',
checkpoints: [{ key: 'container-action', label: 'Host command completed', at, phase: 'container-action' }],
checks: [{ key: 'container', label: 'Container readiness', result: 'waiting', source: 'agent', checkedAt: at, receivedAt: null,
expected: 'healthy', actual: 'unhealthy', message: null }] });
const groups = provisioningGroups(detail);
expect(groups.flatMap(group => group.steps).find(step => step.key === 'container-action')?.state).toBe('done');
expect(groups.find(group => group.key === 'verify')).toMatchObject({ state: 'failed', steps: [
{ key: 'verify-container', state: 'failed', at },
{ key: 'verify-game', state: 'cancelled', at: null },
{ key: 'verify-map', state: 'cancelled', at: null },
{ key: 'verify-plutonium', state: 'cancelled', at: null }
] });
const body = render(ProvisioningProgressView, { props: { detail } }).body;
const phases = body.slice(0, body.indexOf('Latest recorded checks'));
expect(phases.match(/Stopped here/g)).toHaveLength(2);
expect(phases.match(/Not run/g)).toHaveLength(3);
expect(phases).not.toContain('Waiting');
expect(phases).toMatch(/<li class="[^"]*\bfailed\b/);
expect(phases).not.toMatch(/<li class="[^"]*\bcurrent\b/);
expect(phases).not.toContain('aria-current="step"');
expect(detail.progress.checks[0].result).toBe('waiting');
});
it.each(['failed', 'interrupted'])('marks the current step of a %s operation even without conclusive check evidence', state => {
const detail = fixture();
detail.deployment.state = state;
detail.progress = progress();
expect(provisioningGroups(detail).flatMap(group => group.steps).find(step => step.key === 'verify-map')?.state).toBe('failed');
detail.progress.checks = [{ key: 'map', label: 'Engine map', result: 'unknown', source: null, checkedAt: at, receivedAt: null,
expected: 'zombie_theater', actual: null, message: 'No fresh evidence' }];
const steps = provisioningGroups(detail).flatMap(group => group.steps);
expect(steps.find(step => step.key === 'verify-map')?.state).toBe('failed');
expect(steps.find(step => step.key === 'verify-plutonium')?.state).toBe('cancelled');
expect(steps.find(step => step.key === 'prepare')?.state).toBe('unknown');
});
it.each(['done', 'warning', 'cancelled', 'failed', 'interrupted'])('does not highlight stale waiting evidence as active on a %s operation', state => {
const detail = fixture();
detail.progress = progress({ phase: 'verify-container', checks: [
{ key: 'container', label: 'Container readiness', result: 'waiting', source: 'agent', checkedAt: at, receivedAt: null,
expected: 'healthy', actual: 'starting', message: null }
] });
const activeBody = render(ProvisioningProgressView, { props: { detail } }).body;
expect(activeBody).toMatch(/<li class="[^"]*\bcurrent\b/);
expect(activeBody).toContain('aria-current="step"');
detail.deployment.state = state;
detail.deployment.finished_at = at;
const finishedBody = render(ProvisioningProgressView, { props: { detail } }).body;
expect(finishedBody).not.toMatch(/<li class="[^"]*\bcurrent\b/);
expect(finishedBody).not.toContain('aria-current="step"');
});
it.each(['create', 'rebuild'] as const)('shows %s Plutonium verification from runtime evidence, separately from the release manifest', action => {
const detail = fixture();
detail.deployment.action = action;