217 lines
7.7 KiB
TypeScript
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');
|
|
});
|