Consolidate provisioning progress and evidence (Refs #82)
This commit is contained in:
@@ -4,6 +4,7 @@ import type { ProvisioningDetail, ProvisioningProgress } from '../../src/lib/pro
|
||||
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';
|
||||
import ProvisioningActivity from '../../src/lib/ui/components/ProvisioningActivity.svelte';
|
||||
|
||||
const at = '2026-09-28T12:00:00Z';
|
||||
function progress(patch: Partial<ProvisioningProgress> = {}): ProvisioningProgress {
|
||||
@@ -138,7 +139,7 @@ describe('provisioning outcome and evidence presentation', () => {
|
||||
{ key: 'verify-plutonium', state: 'cancelled', at: null }
|
||||
] });
|
||||
const body = render(ProvisioningProgressView, { props: { detail } }).body;
|
||||
const phases = body.slice(0, body.indexOf('Latest recorded checks'));
|
||||
const phases = body;
|
||||
expect(phases.match(/Stopped here/g)).toHaveLength(2);
|
||||
expect(phases.match(/Not run/g)).toHaveLength(3);
|
||||
expect(phases).not.toContain('Waiting');
|
||||
@@ -224,6 +225,108 @@ describe('provisioning outcome and evidence presentation', () => {
|
||||
expect(groups.flatMap(group => group.steps).find(step => step.key === 'container-action')?.state).toBe('done');
|
||||
expect(groups.flatMap(group => group.steps).find(step => step.key === 'cleanup')?.state).toBe('waiting');
|
||||
});
|
||||
|
||||
it('renders check values and their provenance once inside the corresponding step', () => {
|
||||
const detail = fixture();
|
||||
detail.progress = progress({ checks: [
|
||||
{ key: 'game', label: 'Game status', result: 'pass', source: 'rcon', checkedAt: at, receivedAt: at, expected: null, actual: 0, message: null },
|
||||
{ key: 'map', label: 'Engine map', result: 'waiting', source: 'telemetry', checkedAt: at, receivedAt: at, expected: 'zombie_theater', actual: 'zombie_moon', message: 'Waiting for selected map' }
|
||||
] });
|
||||
const steps = provisioningGroups(detail).flatMap(group => group.steps);
|
||||
expect(steps.find(step => step.key === 'verify-game')?.checks).toEqual([detail.progress.checks[0]]);
|
||||
expect(steps.find(step => step.key === 'verify-map')?.checks).toEqual([detail.progress.checks[1]]);
|
||||
const body = render(ProvisioningProgressView, { props: { detail } }).body;
|
||||
const mapStep = body.slice(body.indexOf('Verify selected engine map'), body.indexOf('Verify Plutonium revision'));
|
||||
for (const value of ['Expected', 'Observed', 'zombie_theater', 'zombie_moon', 'Telemetry', 'Evidence received', 'Waiting for selected map']) {
|
||||
expect(mapStep).toContain(value);
|
||||
}
|
||||
expect(body.match(/zombie_theater/g)).toHaveLength(1);
|
||||
expect(body.match(/zombie_moon/g)).toHaveLength(1);
|
||||
expect(body).toContain('0 players');
|
||||
expect(body).not.toContain('Latest recorded checks');
|
||||
});
|
||||
|
||||
it('keeps the release manifest in the revision step without treating it as runtime verification', () => {
|
||||
const detail = fixture();
|
||||
detail.progress = progress({ phase: 'verify-plutonium', checks: [
|
||||
{ key: 'plutonium-release', label: 'Production Plutonium release', result: 'pass', source: 'manifest', checkedAt: at, receivedAt: null, expected: null, actual: 5200, message: null }
|
||||
] });
|
||||
const step = provisioningGroups(detail).flatMap(group => group.steps).find(step => step.key === 'verify-plutonium');
|
||||
expect(step).toMatchObject({ state: 'current', checks: detail.progress.checks });
|
||||
detail.progress.checks.push({ key: 'plutonium-version', label: 'Running Plutonium revision', result: 'waiting', source: 'rcon', checkedAt: at, receivedAt: null, expected: 5200, actual: 5199, message: null });
|
||||
const body = render(ProvisioningProgressView, { props: { detail } }).body;
|
||||
expect(body).toContain('Production Plutonium release');
|
||||
expect(body).toContain('Running Plutonium revision');
|
||||
expect(body).toContain('r5200');
|
||||
expect(body).toContain('r5199');
|
||||
});
|
||||
|
||||
it('folds staging, command receipts, recovery and agent diagnostics into their steps', () => {
|
||||
const detail = fixture();
|
||||
detail.progress = progress({
|
||||
command: { state: 'uncertain', at, message: 'Awaiting acknowledgment of the original command' },
|
||||
checkpoints: [
|
||||
{ key: 'prepare', label: 'Launch files selected and command payload prepared', at, phase: 'prepare' },
|
||||
{ key: 'staging-completed', label: 'File staging completed', at, phase: 'verify-map' },
|
||||
{ key: 'agent-accepted', label: 'Host acknowledged command', at, phase: 'verify-map' },
|
||||
{ key: 'recovery-restart', label: 'Recovery restart completed', at, phase: 'verify-container' },
|
||||
{ key: 'legacy-receipt', label: 'Legacy receipt without a phase', at, phase: null }
|
||||
]
|
||||
});
|
||||
detail.agentOperation = { id: 'agent-command-a', hostId: 'host-a', state: 'unknown', createdAt: at, updatedAt: at, error: 'Agent disconnected' };
|
||||
const steps = provisioningGroups(detail).flatMap(group => group.steps);
|
||||
expect(steps.find(step => step.key === 'prepare')).toMatchObject({ label: detail.progress.checkpoints[0].label, checkpoints: [detail.progress.checkpoints[1]] });
|
||||
expect(steps.find(step => step.key === 'container-action')).toMatchObject({
|
||||
command: detail.progress.command, agentOperation: detail.agentOperation, checkpoints: detail.progress.checkpoints.slice(2)
|
||||
});
|
||||
const body = render(ProvisioningProgressView, { props: { detail, hostHistoryHref: '/hosts/operations?server=test&returnTo=%2Fprovisioning%2Foperation-a' } }).body;
|
||||
const commandStep = body.slice(body.indexOf('Create instance through host agent'), body.indexOf('Verify readiness'));
|
||||
for (const value of ['Remote response uncertain', detail.progress.command!.message, 'agent-command-a', 'Agent disconnected', 'View host operation history']) {
|
||||
expect(commandStep).toContain(value);
|
||||
expect(body.split(value)).toHaveLength(2);
|
||||
}
|
||||
expect(commandStep).toContain('returnTo=%2Fprovisioning%2Foperation-a');
|
||||
expect(body).not.toContain('Recorded checkpoints');
|
||||
for (const checkpoint of detail.progress.checkpoints) expect(body.split(checkpoint.label)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('retains recorded eligibility and staging evidence for actions without launch preparation by default', () => {
|
||||
const detail = fixture();
|
||||
detail.deployment.action = 'restart';
|
||||
detail.progress = progress({ phase: 'eligibility', checks: [
|
||||
{ key: 'occupancy', label: 'Player safety', result: 'waiting', source: 'rcon', checkedAt: at, receivedAt: null, expected: 0, actual: 2, message: 'Players are still connected' }
|
||||
], checkpoints: [{ key: 'staging-completed', label: 'Files staged', at, phase: 'container-action' }] });
|
||||
const groups = provisioningGroups(detail);
|
||||
expect(groups.flatMap(group => group.steps).find(step => step.key === 'eligibility')).toMatchObject({ state: 'waiting', checks: detail.progress.checks });
|
||||
expect(groups.find(group => group.key === 'command')?.steps.map(step => step.key)).toEqual(['prepare', 'container-action']);
|
||||
expect(groups.flatMap(group => group.steps).find(step => step.key === 'prepare')?.checkpoints).toEqual(detail.progress.checkpoints);
|
||||
});
|
||||
|
||||
it('preserves host agent evidence when a legacy operation has no progress snapshot', () => {
|
||||
const detail = fixture();
|
||||
detail.progress = null;
|
||||
detail.agentOperation = { id: 'legacy-agent-command', hostId: 'host-a', state: 'failed', createdAt: at, updatedAt: at, error: 'Command failed on host' };
|
||||
const body = render(ProvisioningProgressView, { props: { detail } }).body;
|
||||
expect(body).toContain('Detailed checkpoints were not recorded');
|
||||
expect(body).toContain('legacy-agent-command');
|
||||
expect(body).toContain('Command failed on host');
|
||||
expect(body).toContain('Stopped here');
|
||||
expect(body).not.toContain('Verify readiness');
|
||||
});
|
||||
|
||||
it.each([0, 1, 5])('keeps %i raw activity events behind one closed disclosure', count => {
|
||||
const detail = fixture();
|
||||
detail.events = Array.from({ length: count }, (_, id) => ({ id, created_at: at, message: 'Raw event message' }));
|
||||
detail.activity.total = count;
|
||||
const body = render(ProvisioningActivity, { props: { detail } }).body;
|
||||
expect(body).toContain(`${count} event${count === 1 ? '' : 's'}`);
|
||||
expect(body.match(/<details\b/g)).toHaveLength(1);
|
||||
expect(body).not.toMatch(/<details\b[^>]*\bopen\b/);
|
||||
expect(body).not.toContain('Recent updates');
|
||||
expect(body).not.toContain('Raw event message');
|
||||
expect(body).toContain('Search activity');
|
||||
expect(body).toContain('Oldest first');
|
||||
});
|
||||
});
|
||||
|
||||
describe('provisioning response and return-context boundaries', () => {
|
||||
|
||||
Reference in New Issue
Block a user