This commit is contained in:
@@ -47,6 +47,26 @@ async function setup() {
|
||||
}
|
||||
|
||||
describe('bounded provisioning lists', () => {
|
||||
it('filters Queued and Waiting independently while preserving combined pending links', async () => {
|
||||
const { add, handle } = await setup();
|
||||
add('queued', 'queued');
|
||||
add('waiting', 'waiting', 'restart');
|
||||
add('running', 'running', 'restart');
|
||||
handle.prepare("UPDATE server_deployments SET phase_key='prepare' WHERE id='queued'").run();
|
||||
const { GET } = await import('../../src/routes/api/provisioning/+server.js');
|
||||
for (const [status, expected] of [
|
||||
['queued', ['queued']],
|
||||
['waiting', ['waiting']],
|
||||
['running', ['running']],
|
||||
['pending', ['waiting', 'queued']]
|
||||
] as const) {
|
||||
const result = await GET({
|
||||
url: new URL(`http://localhost/api/provisioning?view=active&status=${status}`)
|
||||
}).json();
|
||||
expect(result.deployments.map((row: { id: string }) => row.id)).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps recovery sources concise as incident summaries change, including after retention', async () => {
|
||||
const { add, handle, deploymentListPage } = await setup();
|
||||
add('recovery', 'waiting', 'rebuild');
|
||||
@@ -293,13 +313,17 @@ describe('bounded provisioning lists', () => {
|
||||
)
|
||||
.run(
|
||||
JSON.stringify({
|
||||
blocker: { code: 'restart-capacity' },
|
||||
blocker: {
|
||||
code: 'restart-capacity',
|
||||
message: 'Waiting for another server to recover'
|
||||
},
|
||||
checks: ['private snapshot'],
|
||||
checkpoints: []
|
||||
})
|
||||
);
|
||||
expect(deploymentListPage({ view: 'active' }).deployments[0]).toMatchObject({
|
||||
waitCode: 'restart-capacity',
|
||||
waitMessage: 'Waiting for another server to recover',
|
||||
phase: 'eligibility',
|
||||
phaseStartedAt: '2026-01-01'
|
||||
});
|
||||
@@ -308,7 +332,7 @@ describe('bounded provisioning lists', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('puts verification and executing work ahead of preflight waits and capacity queues', async () => {
|
||||
it('orders active jobs by lifecycle state regardless of phases and blockers', async () => {
|
||||
const { add, handle, listDeployments, deploymentListPage } = await setup();
|
||||
const addProgress = (
|
||||
id: string,
|
||||
@@ -403,18 +427,18 @@ describe('bounded provisioning lists', () => {
|
||||
const expected = [
|
||||
'verify-older',
|
||||
'verify-newer',
|
||||
'staging',
|
||||
'staging-wait',
|
||||
'preparing-queued',
|
||||
'capacity',
|
||||
'eligibility',
|
||||
'operation-conflict',
|
||||
'executing',
|
||||
'agent-wait',
|
||||
'cleanup',
|
||||
'eligibility',
|
||||
'legacy-capacity',
|
||||
'host-wait',
|
||||
'operation-conflict',
|
||||
'staging',
|
||||
'staging-wait',
|
||||
'cleanup',
|
||||
'queued',
|
||||
'capacity',
|
||||
'legacy-capacity'
|
||||
'preparing-queued'
|
||||
];
|
||||
expect(listDeployments({ view: 'active' }).map((row) => row.id)).toEqual(expected);
|
||||
const { GET } = await import('../../src/routes/api/provisioning/+server.js');
|
||||
@@ -424,7 +448,7 @@ describe('bounded provisioning lists', () => {
|
||||
expect(active.deployments.map((row: { id: string }) => row.id)).toEqual(expected);
|
||||
expect(active).toMatchObject({ total: expected.length, needsReview: 2 });
|
||||
|
||||
// Progress priority applies only to Active; retained outcomes and mixed reads stay newest first.
|
||||
// State priority applies only to Active; retained outcomes and mixed reads stay newest first.
|
||||
expect(
|
||||
deploymentListPage({ view: 'history', limit: 1 }).deployments.map((row) => row.id)
|
||||
).toEqual(['failed']);
|
||||
|
||||
@@ -1,74 +1,75 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import {
|
||||
provisioningQueueStatus,
|
||||
provisioningWarnings
|
||||
} from '../../src/lib/ui/provisioning-queue.js';
|
||||
import { render } from 'svelte/server';
|
||||
import { provisioningContext } from '../../src/lib/ui/provisioning-presentation.js';
|
||||
import { provisioningStatus, provisioningWarnings } from '../../src/lib/ui/provisioning-queue.js';
|
||||
import ProvisioningStatusBadge from '../../src/lib/ui/components/ProvisioningStatusBadge.svelte';
|
||||
|
||||
it('distinguishes capacity, occupancy, recovery and verification without inventing errors', () => {
|
||||
const row = { action: 'restart' as const, state: 'waiting', stage: 'Waiting' };
|
||||
expect(provisioningQueueStatus({ ...row, waitCode: 'restart-capacity' })).toMatchObject({
|
||||
label: 'Queued for restart',
|
||||
tone: 'neutral',
|
||||
group: 'queued'
|
||||
});
|
||||
expect(
|
||||
provisioningQueueStatus({
|
||||
...row,
|
||||
stage: 'Queued to restart; waiting for another server to recover'
|
||||
})
|
||||
).toMatchObject({ group: 'queued' });
|
||||
expect(
|
||||
provisioningQueueStatus({ ...row, stage: 'Waiting for 3 player(s) to leave.' })
|
||||
).toMatchObject({ label: 'Waiting for empty lobby', group: 'waiting' });
|
||||
expect(
|
||||
provisioningQueueStatus({ ...row, stage: 'Waiting for a fresh local player count.' })
|
||||
).toMatchObject({ label: 'Checking lobby occupancy' });
|
||||
expect(provisioningQueueStatus({ ...row, waitCode: 'operation-conflict' })).toMatchObject({
|
||||
label: 'Waiting for another operation'
|
||||
});
|
||||
expect(
|
||||
provisioningQueueStatus({
|
||||
...row,
|
||||
state: 'awaiting-health',
|
||||
waitCode: 'native-host',
|
||||
stage: 'Waiting for container readiness: starting'
|
||||
})
|
||||
).toMatchObject({ label: 'Verifying health', group: 'verifying' });
|
||||
expect(
|
||||
provisioningQueueStatus({
|
||||
...row,
|
||||
state: 'running',
|
||||
waitCode: 'native-host',
|
||||
stage: 'Waiting for startup capacity.'
|
||||
})
|
||||
).toMatchObject({ label: 'Waiting for capacity', group: 'queued' });
|
||||
expect(provisioningQueueStatus({ ...row, phase: 'cleanup' })).toMatchObject({
|
||||
label: 'Cleaning up',
|
||||
group: 'running'
|
||||
});
|
||||
it.each([
|
||||
['queued', 'Queued', 'neutral'],
|
||||
['waiting', 'Waiting', 'neutral'],
|
||||
['running', 'Running', 'info'],
|
||||
['awaiting-health', 'Validating', 'info'],
|
||||
['done', 'Completed', 'online'],
|
||||
['warning', 'Completed with warnings', 'warning'],
|
||||
['failed', 'Failed', 'danger'],
|
||||
['interrupted', 'Interrupted', 'danger'],
|
||||
['cancelled', 'Cancelled', 'neutral']
|
||||
])('renders the recorded %s lifecycle state consistently', (state, label, tone) => {
|
||||
expect(provisioningStatus(state)).toMatchObject({ label, tone });
|
||||
const body = render(ProvisioningStatusBadge, { props: { state } }).body;
|
||||
expect(body).toContain(label);
|
||||
expect(body).toContain(`v-${tone}`);
|
||||
expect(body).toContain('aria-hidden="true"');
|
||||
});
|
||||
|
||||
it('keeps recorded terminal outcomes ahead of leftover progress and blockers', () => {
|
||||
it('keeps waiting reasons and preparation separate from the recorded state', () => {
|
||||
const row = {
|
||||
state: 'running',
|
||||
action: 'restart' as const,
|
||||
state: 'done',
|
||||
stage: 'Old waiting text',
|
||||
waitCode: 'restart-capacity',
|
||||
phase: 'cleanup'
|
||||
phase: 'container-action',
|
||||
stage: 'Waiting for host acknowledgment',
|
||||
waitMessage: 'Waiting for another operation to finish'
|
||||
};
|
||||
expect(provisioningQueueStatus(row)).toMatchObject({ label: 'Complete', group: 'complete' });
|
||||
expect(provisioningQueueStatus({ ...row, state: 'failed' })).toMatchObject({
|
||||
label: 'Failed',
|
||||
tone: 'danger',
|
||||
group: 'attention'
|
||||
});
|
||||
expect(provisioningQueueStatus({ ...row, state: 'cancelled' })).toMatchObject({
|
||||
label: 'Cancelled',
|
||||
group: 'cancelled',
|
||||
tone: 'neutral'
|
||||
expect(provisioningContext(row)).toEqual({
|
||||
step: 'Restart container',
|
||||
detail: 'Waiting for another operation to finish'
|
||||
});
|
||||
expect(provisioningStatus(row.state).label).toBe('Running');
|
||||
|
||||
const preparing = { ...row, state: 'queued', phase: 'prepare', waitMessage: null };
|
||||
expect(provisioningContext(preparing)?.step).toBe('Prepare server files');
|
||||
expect(provisioningStatus(preparing.state).label).toBe('Queued');
|
||||
});
|
||||
|
||||
it('preserves legacy stage text without parsing it into a lifecycle label', () => {
|
||||
const row = {
|
||||
state: 'waiting',
|
||||
action: 'rebuild' as const,
|
||||
stage: 'Waiting for 3 player(s) to leave.'
|
||||
};
|
||||
expect(provisioningContext(row)).toEqual({ step: row.stage, detail: null });
|
||||
expect(provisioningContext({ ...row, phase: 'future-phase' })).toEqual({
|
||||
step: row.stage,
|
||||
detail: null
|
||||
});
|
||||
expect(provisioningStatus(row.state).label).toBe('Waiting');
|
||||
});
|
||||
|
||||
it.each(['done', 'warning', 'failed', 'interrupted', 'cancelled'])(
|
||||
'does not describe leftover progress as current for %s jobs',
|
||||
(state) => {
|
||||
expect(
|
||||
provisioningContext({
|
||||
state,
|
||||
action: 'restart',
|
||||
stage: 'Waiting for startup capacity',
|
||||
waitMessage: 'Waiting for recovery',
|
||||
phase: 'cleanup'
|
||||
})
|
||||
).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
it('only displays strings from recorded warning arrays', () => {
|
||||
expect(provisioningWarnings('["Warning",null,3]')).toEqual(['Warning']);
|
||||
expect(provisioningWarnings('null')).toEqual([]);
|
||||
|
||||
@@ -123,6 +123,49 @@ function recovery(state: RecoveryObservation['state']): RecoveryObservation {
|
||||
}
|
||||
|
||||
describe('provisioning detail summary and technical details', () => {
|
||||
it('explains file preparation while preserving the Queued lifecycle state', () => {
|
||||
const detail = fixture('queued');
|
||||
detail.deployment.finished_at = null;
|
||||
detail.deployment.stage = 'Preparing launch files for the host agent';
|
||||
detail.progress!.phase = 'prepare';
|
||||
const body = pageBody(detail);
|
||||
const badges = body.match(/<div\b[^>]*class="badges[^>]*>[\s\S]*?<\/div>/)?.[0];
|
||||
expect(badges).toContain('Queued');
|
||||
expect(body).toContain('Current step: Prepare server files');
|
||||
expect(body).toContain('The command has not been submitted yet.');
|
||||
expect(body).not.toContain('awaiting provisioning evaluation');
|
||||
});
|
||||
|
||||
it('shows Validating with the current step and blocker as separate supporting text', () => {
|
||||
const detail = fixture('awaiting-health');
|
||||
detail.deployment.finished_at = null;
|
||||
detail.deployment.stage = 'Checking game status';
|
||||
detail.progress!.blocker = {
|
||||
code: 'native-host',
|
||||
message: 'Waiting for fresh game telemetry',
|
||||
automatic: true
|
||||
};
|
||||
const body = pageBody(detail);
|
||||
const badges = body.match(/<div\b[^>]*class="badges[^>]*>[\s\S]*?<\/div>/)?.[0];
|
||||
expect(badges).toContain('Validating');
|
||||
expect(badges).not.toContain('Waiting for fresh game telemetry');
|
||||
expect(body).toContain('Current step: Verify game status');
|
||||
expect(body).toContain('Waiting for fresh game telemetry');
|
||||
expect(body).not.toContain('Verifying health');
|
||||
});
|
||||
|
||||
it('keeps failed outcomes visible even when a prior blocker remains recorded', () => {
|
||||
const detail = fixture('failed');
|
||||
detail.progress!.blocker = {
|
||||
code: 'startup-capacity',
|
||||
message: 'Waiting for startup capacity',
|
||||
automatic: true
|
||||
};
|
||||
const body = pageBody(detail);
|
||||
expect(body).toContain('Restart failed');
|
||||
expect(body).not.toContain('Current step:');
|
||||
});
|
||||
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user