988 lines
38 KiB
TypeScript
988 lines
38 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).toBeNull();
|
|
expect(render(ProvisioningProgressView, { props: { detail } }).body).toContain(
|
|
'Detailed checkpoints were 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).toBeNull();
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|