Simplify provisioning queue and add shared filters

This commit is contained in:
2026-10-06 16:56:41 -04:00
parent 36c2907602
commit ad5609cd23
10 changed files with 310 additions and 94 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ describe('provisioning detail projection', () => {
expect(detail.activity).toEqual({ total: 0, latestId: null, failureExcerpt: null });
expect(detail.warningMessages).toEqual(['Check files', 'Some recorded warnings could not be read.']);
expect(detail.sources).toEqual([
{ kind: 'campaign', source_id: 'missing', server: 'old-server', label: 'Campaign missing', href: null },
{ kind: 'campaign', source_id: 'missing', server: 'old-server', label: 'Campaign unavailable', href: null },
{ kind: 'watchdog', source_id: 'incident', server: 'old-server', label: 'Recovered game service', href: '/maintenance/incident' }
]);
db().prepare("UPDATE server_deployments SET warnings = 'broken json' WHERE id = 'legacy'").run();
+52 -1
View File
@@ -19,6 +19,57 @@ async function setup() {
}
describe('bounded provisioning lists', () => {
it('filters readable sources before pagination and retains expired source references without broken links', async () => {
const { add, handle, deploymentListPage } = await setup();
handle.exec(`INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'Common scripts','common.gsc');
INSERT INTO script_versions(id,script_id,version,sha256,size) VALUES(1,1,1,'hash',0);
INSERT INTO betas(id,script_id,script_version_id,trigger_kind,state,notes) VALUES(75,1,1,'ui','promoted','Gameplay checks');
INSERT INTO deployments(id,action,started_at,beta_id) VALUES('campaign','promote','2026-01-01',75);`);
add('old-campaign', 'done', 'restart');
handle.prepare("UPDATE server_deployments SET triggered_by='campaign:campaign' WHERE id='old-campaign'").run();
add('linked-beta', 'waiting', 'restart');
handle.prepare("INSERT INTO provisioning_sources(operation_id,kind,source_id,server) VALUES('linked-beta','beta','75','t5_zombie_theater_1')").run();
add('expired', 'done', 'restart');
handle.prepare("UPDATE server_deployments SET triggered_by='campaign:expired' WHERE id='expired'").run();
add('manual', 'done', 'restart');
handle.prepare("UPDATE server_deployments SET triggered_by='ui' WHERE id='manual'").run();
for (let i = 0; i < 30; i++) add(`system-${i}`, 'done', 'restart');
const first = deploymentListPage({ view: 'history', source: 'campaign', action: 'restart', status: 'done', server: 'Kino', limit: 1 });
expect(first.total).toBe(2);
expect(first.deployments[0]).toMatchObject({ id: 'expired', sources: [{ label: 'Campaign unavailable', href: null }] });
const second = deploymentListPage({ view: 'history', source: 'campaign', limit: 1, offset: 1 });
expect(second.deployments[0]).toMatchObject({ id: 'old-campaign', sources: [{ label: 'Promotion · Beta #75', href: '/campaigns/campaign' }] });
const beta = deploymentListPage({ view: 'active', source: 'beta', status: 'pending' });
expect(beta.deployments).toEqual([expect.objectContaining({ id: 'linked-beta', sources: [{ kind: 'beta', source_id: '75', label: 'Beta #75: Gameplay checks', href: '/betas/75' }] })]);
expect(deploymentListPage({ view: 'history', source: 'manual' }).deployments.map(row => row.id)).toEqual(['manual']);
expect(deploymentListPage({ view: 'history', source: 'system' }).total).toBe(30);
const { GET } = await import('../../src/routes/api/provisioning/+server.js');
const result = await GET({ url: new URL('http://localhost/api/provisioning?view=active&status=pending&source=beta') }).json();
expect(result.deployments.map((row: { id: string }) => row.id)).toEqual(['linked-beta']);
});
it('counts unresolved outcomes while retaining resolved failures and routine cancellations in history', async () => {
const { add, handle, deploymentListPage } = await setup();
add('failed', 'failed'); add('resolved', 'failed'); add('cancelled', 'cancelled'); add('warning', 'warning');
handle.prepare("INSERT INTO retention_controls(category,record_id,resolved_at) VALUES('provisioning','resolved','2026-01-02')").run();
expect(deploymentListPage({ view: 'history' }).total).toBe(4);
const attention = deploymentListPage({ view: 'history', status: 'attention' });
expect(attention.needsReview).toBe(2);
expect(attention.deployments.map(row => row.id)).toEqual(['warning', 'failed']);
expect(deploymentListPage({ view: 'history', status: 'failed' }).total).toBe(2);
handle.prepare("UPDATE server_deployments SET finished_at='2026-01-03' WHERE id='resolved'").run();
expect(deploymentListPage({ view: 'history', status: 'attention' }).needsReview).toBe(3);
});
it('exposes compact waiting facts without transferring observation snapshots', async () => {
const { add, handle, deploymentListPage } = await setup();
add('waiting', 'waiting', 'restart');
handle.prepare("UPDATE server_deployments SET phase_key='eligibility',phase_started_at='2026-01-01',observation_json=? WHERE id='waiting'")
.run(JSON.stringify({ blocker: { code: 'restart-capacity' }, checks: ['private snapshot'], checkpoints: [] }));
expect(deploymentListPage({ view: 'active' }).deployments[0]).toMatchObject({ waitCode: 'restart-capacity', phase: 'eligibility', phaseStartedAt: '2026-01-01' });
expect(JSON.stringify(deploymentListPage({ view: 'active' }))).not.toContain('private snapshot');
});
it('polls every active row and counts review outcomes without transferring retained history', async () => {
const { handle, add, REMOVING_SERVER } = await setup();
handle.transaction(() => {
@@ -111,7 +162,7 @@ describe('bounded provisioning lists', () => {
const history = queries.find(sql => sql.includes('AS canCancelRemoval') && sql.includes('LIMIT'))!;
const review = queries.find(sql => sql.includes('COUNT(*) AS needsReview'))!;
expect(explain(active, [REMOVING_SERVER])).toContain('idx_server_deployments_state');
expect(explain(review, [])).toContain('COVERING INDEX idx_server_deployments_state');
expect(explain(review, [])).toContain('idx_server_deployments_state');
const historyPlan = explain(history, [REMOVING_SERVER, 100, 0]);
expect(historyPlan).toContain('idx_server_deployments_started_at');
expect(historyPlan).not.toContain('TEMP B-TREE');
@@ -147,6 +147,8 @@ describe('provisioning response and return-context boundaries', () => {
.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');
}
+27
View File
@@ -0,0 +1,27 @@
import { expect, it } from 'vitest';
import { provisioningQueueStatus, provisioningWarnings } from '../../src/lib/ui/provisioning-queue.js';
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('keeps recorded terminal outcomes ahead of leftover progress and blockers', () => {
const row = { action: 'restart' as const, state: 'done', stage: 'Old waiting text', waitCode: 'restart-capacity', phase: 'cleanup' };
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' });
});
it('only displays strings from recorded warning arrays', () => {
expect(provisioningWarnings('["Warning",null,3]')).toEqual(['Warning']);
expect(provisioningWarnings('null')).toEqual([]);
expect(provisioningWarnings('invalid')).toEqual([]);
});