import { beforeEach, describe, expect, it, vi } from 'vitest' import { mountDashboard } from '../src/dashboard' import { ApiError } from '../src/errors' import type { ApiClient } from '../src/api-client' import type { HostRecord, HostStatus } from '../src/api-schemas' function host(subdomain: string, status: HostStatus): HostRecord { return { hostId: `id-${subdomain}`, accountId: 'acc', subdomain, agentPubkey: new Uint8Array([1]), enrollFpr: `fpr-${subdomain}`, status, lastSeen: '2026-01-01T00:00:00Z', createdAt: '2026-01-01T00:00:00Z', revokedAt: status === 'revoked' ? '2026-01-02T00:00:00Z' : null, } } function fakeApi(listHosts: () => Promise): ApiClient { return { listHosts, getHost: vi.fn(), requestPairingCode: vi.fn(), hostStatus: vi.fn(), issueCapabilityToken: vi.fn(), } as unknown as ApiClient } describe('mountDashboard (T5)', () => { let root: HTMLElement beforeEach(() => { root = document.createElement('div') document.body.append(root) }) it('renders one card per host with the correct status class', async () => { const api = fakeApi(async () => [host('alice', 'online'), host('bob', 'offline'), host('cy', 'draining')]) const dash = mountDashboard(root, api, { pollMs: 100000 }) await dash.refresh() const cards = root.querySelectorAll('.host-card') expect(cards).toHaveLength(3) expect(root.querySelector('.status-online')).not.toBeNull() expect(root.querySelector('.status-draining')).not.toBeNull() dash.dispose() }) it('revoked host is non-interactive: no open action, shows blocked state (INV12 UI)', async () => { const api = fakeApi(async () => [host('gone', 'revoked')]) const dash = mountDashboard(root, api, { pollMs: 100000 }) await dash.refresh() const card = root.querySelector('[data-status="revoked"]') as HTMLElement expect(card.querySelector('.host-open')).toBeNull() expect(card.querySelector('.host-blocked')?.textContent).toBe('revoked') dash.dispose() }) it('open action routes with the host_id (no foreign-host input path exists — INV1/INV3)', async () => { const onOpen = vi.fn() const api = fakeApi(async () => [host('alice', 'online')]) const dash = mountDashboard(root, api, { pollMs: 100000, onOpen }) await dash.refresh() ;(root.querySelector('.host-open') as HTMLButtonElement).click() expect(onOpen).toHaveBeenCalledWith('id-alice') dash.dispose() }) it('a listHosts rejection shows an error banner and keeps polling', async () => { const api = fakeApi(async () => { throw new ApiError('http', 'boom', 500) }) const dash = mountDashboard(root, api, { pollMs: 100000 }) await dash.refresh() expect(root.querySelector('.dashboard-error')?.textContent).toContain('Could not load hosts') dash.dispose() }) it('never issues a request carrying an account_id (INV3) — listHosts is argument-free', async () => { const listHosts = vi.fn(async () => [] as HostRecord[]) const dash = mountDashboard(root, fakeApi(listHosts), { pollMs: 100000 }) await dash.refresh() expect(listHosts).toHaveBeenCalledWith() // no args ⇒ no client-supplied tenant identity dash.dispose() }) })