diff --git a/src/lib/ui/components/ProvisioningProgress.svelte b/src/lib/ui/components/ProvisioningProgress.svelte index 4af5c53..78ded5c 100644 --- a/src/lib/ui/components/ProvisioningProgress.svelte +++ b/src/lib/ui/components/ProvisioningProgress.svelte @@ -20,14 +20,15 @@ {#if groups.length}
    {#each groups as group (group.key)} + {@const defaultOpen = detail.deployment.state !== 'done' || group.steps.some(step => step.state === 'failed' || step.checks.some(check => check.result !== 'pass'))}
  1. -
    +
    { event.preventDefault(); expanded[group.key] = !(expanded[group.key] ?? true); }} + onclick={event => { event.preventDefault(); expanded[group.key] = !(expanded[group.key] ?? defaultOpen); }} onkeydown={event => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); - if (!event.repeat) expanded[group.key] = !(expanded[group.key] ?? true); + if (!event.repeat) expanded[group.key] = !(expanded[group.key] ?? defaultOpen); }} > diff --git a/src/lib/ui/provisioning-presentation.ts b/src/lib/ui/provisioning-presentation.ts index ac6ba75..a376662 100644 --- a/src/lib/ui/provisioning-presentation.ts +++ b/src/lib/ui/provisioning-presentation.ts @@ -125,6 +125,8 @@ export function provisioningOutcome(detail: ProvisioningDetail) { const { deployment: operation, progress } = detail; const active = activeProvisioningStates.includes(operation.state); const failure = ['failed', 'interrupted'].includes(operation.state); + const checksSummary = operation.state === 'done' && progress?.checks.length && progress.checks.every(check => check.result === 'pass') + ? `All ${countLabel(progress.checks.length, 'check')} passed` : null; const headline = operation.state === 'done' ? actionCompleted[operation.action] : operation.state === 'cancelled' ? `${actionLabels[operation.action]} request cancelled` : failure ? `${actionLabels[operation.action]} ${operation.state === 'interrupted' ? 'interrupted' : 'failed'}` @@ -160,7 +162,7 @@ export function provisioningOutcome(detail: ProvisioningDetail) { else description = operation.action === 'destroy' ? 'Removal is in progress. Confirmed checkpoints and remaining cleanup are recorded below.' : operation.action === 'rebuild' ? 'The old instance is removed before a fresh instance is created in its reserved slot. Follow each checkpoint below.' : 'The worker is executing this request and checking its resulting state.'; - return { active, headline, description }; + return { active, headline, description, checksSummary }; } /** Accept return context only from the local operation surfaces we expose. */ diff --git a/src/routes/provisioning/[id]/+page.svelte b/src/routes/provisioning/[id]/+page.svelte index 5b7dd5c..515b5df 100644 --- a/src/routes/provisioning/[id]/+page.svelte +++ b/src/routes/provisioning/[id]/+page.svelte @@ -127,7 +127,10 @@ {#if deployment && detail && outcome}
    {#if deployment.server_deleted_at}Deleted identity{/if}{#if detail.progress?.recovery?.state === 'held'}Recovery capacity held{/if}
    Operation outcome
    -

    {outcome.headline}

    {outcome.description}

    +
    +

    {outcome.headline}

    {outcome.description}

    + {#if outcome.checksSummary}

    {' '}{outcome.checksSummary}

    {/if} +
    {#if deployment.error}{/if} {#if detail.activity.failureExcerpt && detail.activity.failureExcerpt.message !== deployment.error && ['failed', 'interrupted'].includes(deployment.state)}

    Last failure event · {detail.activity.failureExcerpt.message}

    {/if} {#if detail.warningMessages.length}

    Recorded warnings

      {#each detail.warningMessages as warning}
    • {warning}
    • {/each}
    {/if} diff --git a/tests/unit/provisioning-presentation.test.ts b/tests/unit/provisioning-presentation.test.ts index 67760ca..70781b2 100644 --- a/tests/unit/provisioning-presentation.test.ts +++ b/tests/unit/provisioning-presentation.test.ts @@ -20,8 +20,57 @@ function fixture(): ProvisioningDetail { collection: null, progress: progress(), capacity: { evaluatedAt: at, pools: [] }, warningMessages: [] }; } +function completedFixture(): ProvisioningDetail { + const detail = fixture(); + detail.deployment.state = 'done'; + detail.deployment.finished_at = at; + detail.progress = progress({ checks: (['container', 'game', 'map', 'plutonium-release', 'plutonium-version'] as const).map(key => ({ + key, label: key, result: 'pass', source: 'agent', checkedAt: at, receivedAt: at, + expected: `Expected ${key}`, actual: `Observed ${key}`, message: null + })) }); + return detail; +} describe('provisioning outcome and evidence presentation', () => { + it('summarizes recorded passed checks and retains their details in closed timeline groups', () => { + const detail = completedFixture(); + expect(provisioningOutcome(detail).checksSummary).toBe('All 5 checks passed'); + const body = render(ProvisioningProgressView, { props: { detail } }).body; + expect(body.match(/]*\bopen\b/); + for (const check of detail.progress!.checks) { + expect(body).toContain(check.expected); + expect(body).toContain(check.actual); + } + expect(body).toContain('Host agent'); + expect(body).toContain('Evidence received'); + detail.progress!.checks = detail.progress!.checks.slice(0, 1); + expect(provisioningOutcome(detail).checksSummary).toBe('All 1 check passed'); + detail.progress!.checks = []; + expect(provisioningOutcome(detail).checksSummary).toBeNull(); + detail.progress = null; + expect(provisioningOutcome(detail).checksSummary).toBeNull(); + }); + + it.each(['fail', 'waiting', 'unknown'] as const)('keeps %s evidence visible even with a recorded completed outcome', result => { + const detail = completedFixture(); + detail.progress!.checks[2].result = result; + expect(provisioningOutcome(detail).checksSummary).toBeNull(); + const body = render(ProvisioningProgressView, { props: { detail } }).body; + const openGroups = body.match(/]*\bopen\b[^>]*>[\s\S]*?<\/details>/g); + expect(openGroups).toHaveLength(1); + expect(openGroups![0]).toContain('Observed map'); + }); + + it.each(['queued', 'waiting', 'running', 'awaiting-health', 'warning', 'failed', 'interrupted', 'cancelled'])( + 'keeps %s evidence expanded without claiming the operation passed all checks', state => { + const detail = completedFixture(); + detail.deployment.state = state; + expect(provisioningOutcome(detail).checksSummary).toBeNull(); + const body = render(ProvisioningProgressView, { props: { detail } }).body; + expect(body.match(/]*\bopen\b/g)).toHaveLength(3); + }); + it('shows recorded usernames and preserves unknown manual requesters', () => { expect(provisioningRequester({ triggered_by: 'ui', requested_by: 'alice' })).toBe('alice'); expect(provisioningRequester({ triggered_by: 'rebalance:run', requested_by: 'bob' })).toBe('bob');