Files
2026-10-11 23:30:40 -04:00

217 lines
7.7 KiB
TypeScript

import { beforeEach, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import { writable } from 'svelte/store';
import { defaultPlan, requirement, type BetaTestView } from '../../src/lib/beta-plan.js';
import { betaStatus, type BetaDeploymentServer } from '../../src/lib/ui/betas.js';
import BetaTestProgress from '../../src/lib/ui/components/BetaTestProgress.svelte';
import BetaList from '../../src/routes/betas/+page.svelte';
import NewBeta from '../../src/routes/betas/new/+page.svelte';
import type { UserRole } from '../../src/lib/auth.js';
const currentPage = writable({
url: new URL('http://localhost/betas/75'),
data: { user: { role: 'admin' as UserRole } }
});
vi.mock('$app/stores', () => ({
page: {
subscribe: (run: Parameters<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
}
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn() }));
function asRole(role: UserRole) {
currentPage.update((value) => ({ ...value, data: { user: { role } } }));
}
beforeEach(() => asRole('admin'));
it('prioritizes lifecycle, failures and pauses over readiness without treating stage text as failure', () => {
expect(betaStatus({ state: 'promoted', testOutcome: 'paused', servers: [] })).toMatchObject({
key: 'promoted',
label: 'Promoted'
});
expect(betaStatus({ state: 'rolled-back', testOutcome: 'failed', servers: [] })).toMatchObject({
key: 'rolled-back'
});
expect(
betaStatus({ state: 'ready-promote', testOutcome: 'failed', servers: [] })
).toMatchObject({ key: 'attention', tone: 'danger' });
expect(
betaStatus({ state: 'ready-promote', testOutcome: 'paused', servers: [] })
).toMatchObject({ key: 'paused' });
expect(
betaStatus({ state: 'ready-promote', testOutcome: 'passed', servers: [{ stage: 'ready' }] })
).toMatchObject({ key: 'ready' });
expect(betaStatus({ state: 'running', servers: [{ stage: 'awaiting-empty' }] })).toMatchObject({
key: 'testing'
});
expect(
betaStatus({
state: 'running',
servers: [{ stage: 'ready', server_deleted_at: '2026-10-06' }]
})
).toMatchObject({ key: 'attention' });
});
const deployment = (server: string, stage = 'ready'): BetaDeploymentServer => ({
server,
stage,
error: null,
deployed_at: '2026-10-06',
restarted_at: '2026-10-06',
healthy_at: '2026-10-06',
match_seen_at: null,
provisioning_id: null
});
const testRun = (
server: string,
passed = true,
failed = false
): BetaTestView['servers'][number] => ({
server,
server_deleted_at: null,
phase: 1,
passed,
observingAt: '2026-10-06',
progress: [
{
id: 'health',
label: 'Health',
current: passed ? 1 : 0,
target: 1,
passed,
failed,
detail: failed ? 'Health check failed' : 'Health observation'
}
]
});
it('combines deployment and tests once per server, expands failures first, and keeps passing details collapsed', () => {
const test: BetaTestView = {
plan: defaultPlan(),
outcome: 'failed',
reason: null,
servers: [testRun('passed'), testRun('failed', false, true)],
evidence: []
};
const body = render(BetaTestProgress, {
props: {
test,
betaId: 75,
closed: false,
deployments: [
deployment('passed'),
deployment('waiting', 'awaiting-empty'),
deployment('failed')
],
onUpdate: async () => {}
}
}).body;
expect(body).toContain('Deployment</th>');
expect(body).toContain('Test result</th>');
expect(body).toContain('Awaiting test run');
expect(body.indexOf('id="beta-server-75-failed"')).toBeLessThan(
body.indexOf('id="beta-server-75-passed"')
);
expect(body).toMatch(/<tr hidden[^>]*id="beta-server-75-passed"/);
expect(body).toMatch(/<tr id="beta-server-75-failed"/);
expect(body.match(/id="beta-server-75-passed"/g)).toHaveLength(1);
expect(body).toContain('View relevant evidence');
expect(body).not.toContain('Search evidence');
});
it('keeps manual sign-offs in server details and disables them on closed or deleted tests', () => {
const test: BetaTestView = {
plan: { ...defaultPlan(), requirements: [requirement('manual', 'health')] },
outcome: 'active',
reason: null,
servers: [testRun('manual', false)],
evidence: []
};
const props = {
test,
betaId: 75,
closed: false,
deployments: [deployment('manual')],
onUpdate: async () => {}
};
expect(render(BetaTestProgress, { props }).body).toContain('Tester name and observation');
expect(render(BetaTestProgress, { props: { ...props, closed: true } }).body).not.toContain(
'Tester name and observation'
);
const deleted = render(BetaTestProgress, {
props: {
...props,
deletedParticipants: true,
deployments: [{ ...deployment('manual'), server_deleted_at: '2026-10-06' }]
}
}).body;
expect(deleted).not.toContain('Tester name and observation');
expect(deleted).not.toContain('href="/servers/manual');
});
it('shows missing historical evidence as not recorded, without claiming zero servers passed', () => {
const test: BetaTestView = {
plan: defaultPlan(),
outcome: 'passed',
reason: null,
servers: [],
evidence: []
};
const body = render(BetaTestProgress, {
props: {
test,
betaId: 75,
closed: true,
deployments: [deployment('historical')],
onUpdate: async () => {}
}
}).body;
expect(body).toContain('Not recorded');
expect(body).not.toContain('Awaiting test run');
expect(body).not.toContain('0 / 0');
});
it.each(['super_viewer', 'viewer'] as const)(
'keeps beta evidence available to %s without pause, retry or sign-off controls',
(role) => {
asRole(role);
const test: BetaTestView = {
plan: { ...defaultPlan(), requirements: [requirement('manual', 'health')] },
outcome: 'active',
reason: null,
servers: [testRun('manual', false)],
evidence: []
};
for (const outcome of ['active', 'paused', 'failed'] as const) {
const body = render(BetaTestProgress, {
props: {
test: { ...test, outcome },
betaId: 75,
closed: false,
onUpdate: async () => {}
}
}).body;
expect(body).toContain('Test results');
expect(body).toContain('Test policy');
expect(body).toContain('View relevant evidence');
expect(body).not.toContain('Tester name and observation');
expect(body).not.toContain('Sign off</button>');
expect(body).not.toContain('Pause testing');
expect(body).not.toContain('Resume testing');
expect(body).not.toContain('Start fresh attempt');
}
}
);
it('hides new-beta entry points from Super Viewers and explains directly opened creation pages', () => {
asRole('super_viewer');
expect(render(BetaList).body).not.toContain('href="/betas/new"');
const creation = render(NewBeta).body;
expect(creation).toContain('Beta creation requires operator access');
expect(creation).toContain('View betas');
expect(creation).not.toContain('<form');
asRole('operator');
expect(render(BetaList).body).toContain('href="/betas/new"');
expect(render(NewBeta).body).not.toContain('Beta creation requires operator access');
});