Files
electric-cherry/tests/unit/provisioning-presentation.test.ts
T

441 lines
30 KiB
TypeScript

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';
import ProvisioningActivity from '../../src/lib/ui/components/ProvisioningActivity.svelte';
const at = '2026-09-28T12:00:00Z';
function progress(patch: Partial<ProvisioningProgress> = {}): ProvisioningProgress {
return { version: 1, phase: 'verify-map', phaseStartedAt: at, observedAt: at, blocker: null, checks: [], checkpoints: [],
command: null, recovery: null, recipeStatus: null, ...patch };
}
function fixture(): ProvisioningDetail {
return {
deployment: { id: 'operation-a', server: 't5_zombie_theater_1', action: 'create', state: 'awaiting-health', stage: 'Waiting for a map',
server_deleted_at: null, server_exists: 1, started_at: at, finished_at: null, triggered_by: 'ui', error: null, warnings: '[]' },
generatedAt: at, events: [], activity: { total: 0, latestId: null, failureExcerpt: null }, sources: [],
recipe: null, blueprintName: null, canResumeCleanup: false, canResumeRebuild: false, canResumeVerification: false, canCancelRemoval: false,
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(/<details\b/g)).toHaveLength(3);
expect(body).not.toMatch(/<details\b[^>]*\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(/<details\b[^>]*\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(/<details\b[^>]*\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');
expect(provisioningRequester({ triggered_by: 'ui', requested_by: null })).toBe('Not recorded');
expect(provisioningRequester({ triggered_by: 'ui' })).toBe('Not recorded');
});
it.each(['watchdog:incident', 'maintenance:job', 'autoscaling:policy', 'campaign:run', 'beta:75', 'rebalance:run', 'host-drain'])(
'shows Maxis for system work from %s', triggered_by => {
expect(provisioningRequester({ triggered_by, requested_by: null })).toBe('Maxis');
expect(provisioningRequester({ triggered_by })).toBe('Maxis');
});
it('distinguishes a failed creation retry from readiness verification and its linked recovery', () => {
const detail = fixture();
detail.deployment.state = 'failed';
detail.canRetryWithRebuild = true;
expect(provisioningOutcome(detail)).toMatchObject({ active: false, headline: 'Create failed' });
expect(provisioningOutcome(detail).description).toContain('new attempt for the same lobby and port');
detail.canRetryWithRebuild = false;
detail.progress = progress({ recovery: { state: 'transferred', operationId: 'recovery-a', at, message: 'Rebuild requested' } });
expect(provisioningOutcome(detail).headline).toBe('Create failed');
expect(provisioningOutcome(detail).description).toContain('linked operation');
detail.progress = progress();
detail.canResumeVerification = true;
expect(provisioningOutcome(detail).description).toContain('remaining restart and rebuild recovery steps');
});
it('distinguishes cancellation from confirmed cleanup and validates its link data', () => {
const detail = fixture();
detail.deployment.state = 'cancelled';
detail.canCancelCreation = false;
detail.cancellation = { cleanupId: 'cleanup-a', state: 'running', stage: 'Waiting for host', error: null };
expect(isProvisioningDetail(detail, detail.deployment.id)).toBe(true);
expect(provisioningOutcome(detail).description).toContain('ports remain reserved');
detail.cancellation.state = 'done';
expect(provisioningOutcome(detail).description).toContain('have been removed');
expect(isProvisioningDetail({ ...detail, cancellation: { state: 'done' } }, detail.deployment.id)).toBe(false);
});
it.each(provisioningActions)('describes cancelled %s as terminal, with no automatic execution claim', action => {
const detail = fixture();
detail.deployment.action = action;
detail.deployment.state = 'cancelled';
const outcome = provisioningOutcome(detail);
expect(outcome.active).toBe(false);
expect(outcome.headline).toContain('cancelled');
expect(outcome.description).toContain('remaining work will not run');
expect(outcome.description).not.toMatch(/verifying|automatically/);
});
it('keeps backend-authorized cleanup actionable and active after identity deletion', () => {
const detail = fixture();
detail.deployment.action = 'destroy';
detail.deployment.state = 'failed';
detail.deployment.server_deleted_at = at;
detail.deployment.server_exists = 0;
detail.canResumeCleanup = true;
expect(provisioningOutcome(detail).description).toContain('Resume the remaining local cleanup');
detail.deployment.state = 'waiting';
detail.canResumeCleanup = false;
detail.progress = progress({ phase: 'cleanup', checks: [] });
expect(provisioningOutcome(detail).active).toBe(true);
expect(provisioningOutcome(detail).description).toContain('without repeating container removal');
expect(provisioningOutcome(detail).description).not.toContain('eligibility');
});
it('does not invent completed phases from a later phase or final outcome', () => {
const detail = fixture();
const steps = provisioningGroups(detail).flatMap(group => group.steps);
expect(steps.find(step => step.key === 'prepare')?.state).toBe('unknown');
expect(steps.find(step => step.key === 'verify-map')?.state).toBe('current');
detail.deployment.state = 'done';
expect(provisioningGroups(detail).flatMap(group => group.steps).every(step => step.state === 'unknown')).toBe(true);
detail.progress = null;
expect(provisioningGroups(detail)).toEqual([]);
expect(provisioningOutcome(detail).description).toContain('evidence was not recorded');
});
it('shows passed container and telemetry game checks while waiting on the engine map', () => {
const detail = fixture();
detail.progress = progress({ checks: [
{ key: 'container', label: 'Container readiness', result: 'pass', source: 'agent', checkedAt: at, receivedAt: null, expected: 'healthy', actual: 'healthy', message: null },
{ key: 'game', label: 'Game status', result: 'pass', source: 'telemetry', checkedAt: at, receivedAt: at, expected: null, actual: null, message: 'Fresh telemetry accepted' },
{ key: 'map', label: 'Engine map', result: 'waiting', source: 'telemetry', checkedAt: at, receivedAt: at, expected: 'zombie_theater', actual: 'zombie_moon', message: null }
] });
const steps = provisioningGroups(detail).flatMap(group => group.steps);
expect(steps.find(step => step.key === 'verify-container')?.state).toBe('done');
expect(steps.find(step => step.key === 'verify-game')?.state).toBe('done');
expect(steps.find(step => step.key === 'verify-map')?.state).toBe('waiting');
expect(steps.find(step => step.key === 'verify-game')?.label).not.toContain('RCON');
detail.deployment.state = 'done';
expect(provisioningOutcome(detail).description).not.toContain('RCON');
});
it('newer unknown readiness evidence supersedes an old completion checkpoint', () => {
const detail = fixture();
detail.progress = progress({ checkpoints: [{ key: 'verify-game', label: 'Game passed', at, phase: 'verify-game' }],
checks: [{ key: 'game', label: 'Game status', result: 'unknown', source: null, checkedAt: '2026-09-28T12:01:00Z', receivedAt: null, expected: null, actual: null, message: 'No fresh evidence' }] });
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;
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;
detail.progress = progress({ phase: 'verify-plutonium',
checks: [
{ key: 'plutonium-release', label: 'Production Plutonium release', result: 'pass', source: 'manifest', checkedAt: at, receivedAt: at, expected: null, actual: 5200, message: null },
{ key: 'plutonium-version', label: 'Running Plutonium revision', result: 'unknown', source: 'rcon', checkedAt: at, receivedAt: null, expected: 5200, actual: null, message: 'Waiting for a runtime revision' }
] });
expect(isProvisioningDetail(detail, detail.deployment.id)).toBe(true);
for (const [result, state] of [['unknown', 'unknown'], ['waiting', 'waiting'], ['fail', 'failed'], ['pass', 'done']] as const) {
detail.progress.checks[1].result = result;
const steps = provisioningGroups(detail).find(group => group.key === 'verify')!.steps;
expect(steps.at(-1)).toMatchObject({ key: 'verify-plutonium', label: 'Verify Plutonium revision', state, at });
}
});
it.each(['start', 'restart', 'stop', 'destroy'] as const)('does not add Plutonium verification to %s', action => {
const detail = fixture();
detail.deployment.action = action;
expect(provisioningGroups(detail).flatMap(group => group.steps).some(step => step.key === 'verify-plutonium')).toBe(false);
});
it('does not claim historical creations verified Plutonium without evidence', () => {
const detail = fixture();
detail.deployment.state = 'done';
detail.progress = progress({ checkpoints: [{ key: 'verify-map', label: 'Selected map verified', at, phase: 'verify-map' }] });
expect(provisioningGroups(detail).flatMap(group => group.steps).find(step => step.key === 'verify-plutonium'))
.toMatchObject({ state: 'unknown', at: null });
});
it('preserves the failed request when later recovery checks pass', () => {
const detail = fixture();
detail.deployment.action = 'restart'; detail.deployment.state = 'failed';
detail.progress = progress({ phase: 'verify-game', recovery: { state: 'released', at, message: 'Recovery verified and capacity released.' },
checks: [{ key: 'game', label: 'Game status', result: 'pass', source: 'rcon', checkedAt: at, receivedAt: null, expected: null, actual: 0, message: null }] });
expect(provisioningOutcome(detail).headline).toBe('Restart failed');
expect(provisioningGroups(detail).flatMap(group => group.steps).find(step => step.key === 'verify-game')?.state).toBe('done');
});
it('keeps confirmed agent removal separate from registry cleanup', () => {
const detail = fixture(); detail.deployment.action = 'destroy'; detail.deployment.state = 'waiting';
detail.progress = progress({ phase: 'cleanup', checkpoints: [{ key: 'container-action', label: 'Agent confirmed removal', at, phase: 'container-action' }] });
const groups = provisioningGroups(detail);
expect(groups.map(group => group.key)).toEqual(['request', 'command', 'cleanup']);
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 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('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', () => {
it('accepts optional retry eligibility only as an authoritative boolean', () => {
const detail = fixture();
for (const canRetryWithRebuild of [undefined, false, true]) {
expect(isProvisioningDetail({ ...detail, canRetryWithRebuild }, detail.deployment.id)).toBe(true);
}
for (const canRetryWithRebuild of ['true', 1, null]) {
expect(isProvisioningDetail({ ...detail, canRetryWithRebuild }, detail.deployment.id)).toBe(false);
}
});
it('accepts persisted usernames and legacy details but rejects malformed requesters', () => {
const detail = fixture();
for (const requested_by of [undefined, null, 'alice']) {
expect(isProvisioningDetail({ ...detail, deployment: { ...detail.deployment, requested_by } }, detail.deployment.id)).toBe(true);
}
expect(isProvisioningDetail({ ...detail, deployment: { ...detail.deployment, requested_by: 42 } }, detail.deployment.id)).toBe(false);
});
it('retains list filters without accepting an external or unrelated return destination', () => {
expect(provisioningReturn('/provisioning?view=history&action=restart&server=Lobby+1&limit=50&redirect=https://evil.test').href)
.toBe('/provisioning?view=history&action=restart&server=Lobby+1&limit=50');
expect(provisioningReturn('/provisioning?view=history&limit=25&offset=100').href)
.toBe('/provisioning?view=history&limit=25&offset=100');
expect(provisioningReturn('/provisioning?view=history&source=campaign&status=failed&offset=25').href)
.toBe('/provisioning?view=history&status=failed&source=campaign&offset=25');
for (const value of ['https://evil.test', '//evil.test', '/\\evil.test', '/\n/evil.test', '/settings', '/provisioning/other']) {
expect(provisioningReturn(value).href).toBe('/provisioning');
}
expect(provisioningReturn('/collections/launches/launch-a').href).toBe('/collections/launches/launch-a');
});
it('returns rebalance moves to their run while discarding unrelated navigation parameters', () => {
expect(provisioningReturn('/capacity?rebalance=1&rebalanceRun=run-a&redirect=https://evil.test'))
.toEqual({ href: '/capacity?rebalance=1&rebalanceRun=run-a', label: 'Back to rebalance' });
expect(provisioningReturn('/capacity?rebalance=1')).toEqual({ href: '/capacity?rebalance=1', label: 'Back to rebalance' });
expect(provisioningReturn('/capacity?rebalanceRun=run-a').href).toBe('/provisioning');
const detail = fixture();
detail.sources = [{ kind: 'rebalance', source_id: 'run-a', label: 'Rebalance', href: '/capacity?rebalance=1&rebalanceRun=run-a' }];
expect(isProvisioningDetail(detail, detail.deployment.id)).toBe(true);
detail.sources[0].href = '//evil.test';
expect(isProvisioningDetail(detail, detail.deployment.id)).toBe(false);
});
it('rejects incomplete nested snapshots before they replace a working detail', () => {
const detail = fixture();
expect(isProvisioningDetail(detail, detail.deployment.id)).toBe(true);
expect(isProvisioningDetail(detail, 'another-id')).toBe(false);
for (const patch of [
{ progress: undefined }, { progress: {} }, { progress: { ...detail.progress, checks: [null] } },
{ capacity: { evaluatedAt: at, pools: [{ kind: 'restart', holders: null }] } }, { activity: { total: 0 } },
{ warningMessages: {} }, { recipe: { files: [] } }, { sources: [{ kind: 'campaign', label: 'Campaign', source_id: 'a', href: 'javascript:alert(1)' }] }
]) expect(isProvisioningDetail({ ...detail, ...patch }, detail.deployment.id)).toBe(false);
expect(isProvisioningDetail({ ...detail, progress: { ...detail.progress, command: { state: 'rejected', at, message: 'Rejected before submission' } } }, detail.deployment.id)).toBe(true);
});
it('rejects an invalid history event without dropping the existing history view', () => {
const page = { events: [{ id: 1, created_at: at, message: 'Queued' }], total: 1, totalEvents: 1, latestId: 1, watermark: 1, nextCursor: null };
expect(isProvisioningEventPage(page)).toBe(true);
expect(isProvisioningEventPage({ ...page, events: [{ id: 1, created_at: 'invalid', message: 'Queued' }] })).toBe(false);
});
});