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[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'); expect(body).toContain('Test result'); 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(/]*id="beta-server-75-passed"/); expect(body).toMatch(/ { 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'); 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('