Files
electric-cherry/tests/unit/ui-provisioning-detail.test.ts
T
2026-10-11 23:30:40 -04:00

240 lines
9.3 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import { writable } from 'svelte/store';
import type { AuthUser } from '../../src/lib/auth.js';
import type { ProvisioningDetail, RecoveryObservation } from '../../src/lib/provisioning-detail.js';
import { provisioningNeedsRefresh } from '../../src/lib/ui/provisioning-presentation.js';
import ProvisioningPage from '../../src/routes/provisioning/[id]/+page.svelte';
vi.mock('$app/stores', () => ({
page: writable({
url: new URL('http://localhost/provisioning/operation-a'),
data: { user: null }
})
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
const start = '2026-10-09T12:00:00Z';
const finish = '2026-10-09T12:01:00Z';
function fixture(state = 'done', recovery: RecoveryObservation | null = null): ProvisioningDetail {
return {
deployment: {
id: 'operation-a',
server: 't5_zombie_theater_1',
action: 'restart',
state,
stage: 'Completed',
server_deleted_at: null,
server_exists: 1,
started_at: start,
finished_at: finish,
triggered_by: 'watchdog:incident-a',
requested_by: 'Maxis',
error: null,
warnings: '[]'
},
generatedAt: finish,
events: [],
activity: { total: 0, latestId: null, failureExcerpt: null },
sources: [
{
kind: 'watchdog',
source_id: 'incident-a',
label: 'Deleted watchdog incident',
href: null
}
],
recipe: null,
blueprintName: null,
canResumeCleanup: false,
canResumeRebuild: false,
canResumeVerification: false,
canCancelRemoval: false,
collection: { id: 'collection-a', name: 'Evening lobbies' },
progress: {
version: 1,
phase: 'verify-game',
phaseStartedAt: '2026-10-09T12:00:50Z',
observedAt: finish,
blocker: null,
checks: [],
checkpoints: [],
command: null,
recovery,
recipeStatus: null
},
agentOperation: {
id: 'command-a',
hostId: 'host-a',
state: 'completed',
createdAt: start,
updatedAt: finish,
error: null
},
capacity: { evaluatedAt: finish, pools: [] },
warningMessages: []
};
}
function pageBody(detail: ProvisioningDetail) {
return render(ProvisioningPage, {
props: { data: { detail, user: null, setupRequired: false } }
}).body;
}
it('lets Super Viewers inspect actionable operations without mutation controls', () => {
const detail = fixture('failed');
Object.assign(detail, {
canResumeVerification: true,
canResumeCleanup: true,
canResumeRebuild: true,
canRetryWithRebuild: true,
canCancel: true,
canCancelCreation: true,
canOverrideSafety: true
});
const bodyFor = (role: AuthUser['role']) =>
render(ProvisioningPage, {
props: { data: { detail, user: { role } as AuthUser, setupRequired: false } }
}).body;
const readOnly = bodyFor('super_viewer');
const admin = bodyFor('admin');
expect(readOnly).toContain('View server');
expect(readOnly).toContain('operation-a');
for (const action of [
'Resume health verification',
'Resume rebuild cleanup',
'Resume cleanup',
'Retry with rebuild',
'Cancel operation',
'Cancel creation and remove server',
'Admin override…'
]) {
expect(readOnly).not.toContain(action);
expect(admin).toContain(action);
}
});
function recovery(state: RecoveryObservation['state']): RecoveryObservation {
return {
state,
at: finish,
message: 'Recovery status updated',
...(state === 'transferred' ? { operationId: 'rebuild-a' } : {})
};
}
describe('provisioning detail summary and technical details', () => {
it('puts requester, absolute start and one duration above the outcome', () => {
const body = pageBody(fixture());
const summary = body.match(/<p\b[^>]*class="operation-summary[^>]*>[\s\S]*?<\/p>/)?.[0];
expect(summary).toBeDefined();
expect(summary).toContain('Requested by Maxis');
expect(summary).toContain('datetime="2026-10-09T12:00:00.000Z"');
expect(summary).toContain('Oct 9, 2026');
expect(summary).toContain('took 1m 0s');
expect(body.indexOf(summary!)).toBeLessThan(body.indexOf('id="outcome-title"'));
expect(body.match(/1m 0s/g)).toHaveLength(1);
expect(body).not.toContain('Recorded duration');
expect(body).not.toContain('Operation outcome');
expect(body).not.toContain('Updates every 5 seconds');
});
it('keeps operation, command and source IDs and copy controls in closed Technical details', () => {
const body = pageBody(fixture());
const technical = body.match(
/<details\b[^>]*class="technical-details[^>]*>[\s\S]*?<\/details>/
)?.[0];
expect(technical).toBeDefined();
expect(technical).toContain('Technical details</summary>');
expect(technical!.split('>')[0]).not.toMatch(/\bopen\b/);
const outside = body.replace(technical!, '');
for (const [value, label] of [
['operation-a', 'operation ID'],
['command-a', 'agent command ID'],
['collection-a', 'collection launch ID'],
['incident-a', 'Deleted watchdog incident source ID']
]) {
const code = new RegExp(`<code\\b[^>]*>${value}</code>`);
expect(technical).toMatch(code);
expect(outside).not.toMatch(code);
expect(technical).toContain(`aria-label="Copy ${label}"`);
expect(outside).not.toContain(`aria-label="Copy ${label}"`);
}
expect(technical).toContain('Copy link');
expect(outside).not.toContain('Copy link');
expect(technical).toContain('datetime="2026-10-09T12:01:00.000Z"');
expect(outside).toContain('href="/collections/launches/collection-a"');
expect(outside).toContain('Source record no longer available');
});
it('shows one elapsed duration for active work and preserves phase timing and freshness', () => {
const detail = fixture('running');
detail.deployment.finished_at = null;
const body = pageBody(detail);
expect(body).toContain('1m 0s elapsed');
expect(body.match(/1m 0s/g)).toHaveLength(1);
expect(body).not.toContain('Total elapsed');
expect(body).toContain('Time in phase');
expect(body).toContain('10s');
expect(body).toContain('Last evaluated');
expect(body).toContain('Updates every 5 seconds');
});
it('does not invent a duration when the finish time is missing', () => {
const detail = fixture('interrupted');
detail.deployment.finished_at = null;
const body = pageBody(detail);
expect(body).toContain('Duration: Not recorded');
expect(body).not.toContain('took ');
expect(body).not.toContain(' elapsed');
expect(body).not.toContain('Updates every 5 seconds');
});
});
describe('provisioning refresh policy', () => {
it.each(['queued', 'waiting', 'running', 'awaiting-health'])(
'refreshes %s operations',
(state) => {
expect(provisioningNeedsRefresh(fixture(state))).toBe(true);
}
);
it.each(['done', 'failed', 'interrupted', 'warning', 'cancelled'])(
'stops refreshing %s operations without held recovery',
(state) => {
const detail = fixture(state);
expect(provisioningNeedsRefresh(detail)).toBe(false);
for (const value of ['released', 'transferred', 'unknown'] as const) {
detail.progress!.recovery = recovery(value);
expect(provisioningNeedsRefresh(detail)).toBe(false);
}
detail.progress = null;
expect(provisioningNeedsRefresh(detail)).toBe(false);
}
);
it.each(['released', 'transferred'] as const)(
'keeps failed restart recovery refreshing until %s without marking it live',
(state) => {
const detail = fixture('failed', recovery('held'));
expect(provisioningNeedsRefresh(detail)).toBe(true);
const held = pageBody(detail);
expect(held).toContain('Recovery capacity held');
expect(held).not.toContain('Updates every 5 seconds');
expect(held).not.toContain('View recovery operation');
detail.progress!.recovery = recovery(state);
expect(provisioningNeedsRefresh(detail)).toBe(false);
const settled = pageBody(detail);
expect(settled).not.toContain('Recovery capacity held');
expect(settled).not.toContain('Updates every 5 seconds');
expect(settled).toContain(
state === 'released' ? 'Recovery capacity released' : 'Recovery transferred'
);
if (state === 'transferred') {
expect(settled).toContain('href="/provisioning/rebuild-a"');
expect(settled).toContain('View recovery operation');
}
}
);
});