Align provisioning UI with lifecycle states
CI / checks (push) Successful in 5m37s

This commit is contained in:
2026-10-12 15:52:34 -04:00
parent 6059ebf2a5
commit c296769b33
15 changed files with 294 additions and 237 deletions
+62 -61
View File
@@ -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([]);