import { beforeEach, describe, expect, it, vi } from 'vitest' import { mountAddMachine } from '../src/add-machine' import { ApiError } from '../src/errors' import type { ApiClient } from '../src/api-client' import type { HostRecord } from '../src/api-schemas' function host(subdomain: string, status: HostRecord['status'] = 'online'): 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: null, } } function fakeApi(over: Partial): ApiClient { return { listHosts: vi.fn(async () => []), getHost: vi.fn(), requestPairingCode: vi.fn(), hostStatus: vi.fn(), issueCapabilityToken: vi.fn(), ...over, } as unknown as ApiClient } const flush = () => new Promise((r) => setTimeout(r, 0)) const tick = (ms: number) => new Promise((r) => setTimeout(r, ms)) describe('mountAddMachine (T6)', () => { let root: HTMLElement beforeEach(() => { root = document.createElement('div') document.body.append(root) }) it('happy path: shows npx command, detects a newly online host, fires onPaired once', async () => { let call = 0 const listHosts = vi.fn(async () => (call++ === 0 ? [] : [host('newbox')])) const requestPairingCode = vi.fn(async () => ({ code: 'ABCD-1234', expiresAt: futureIso() })) const onPaired = vi.fn() const api = fakeApi({ listHosts, requestPairingCode }) const add = mountAddMachine(root, api, { pollMs: 5, onPaired }) await add.start() await flush() await flush() await flush() expect(root.querySelector('.pair-command')?.textContent).toBe( 'npx web-terminal-agent pair ABCD-1234', ) expect(onPaired).toHaveBeenCalledExactlyOnceWith('id-newbox') add.dispose() }) it('expired code: after expiresAt it shows "expired", clears the code, and stops polling', async () => { let t = 1_000_000 const requestPairingCode = vi.fn(async () => ({ code: 'EXP-0001', expiresAt: new Date(t + 50).toISOString() })) const listHosts = vi.fn(async () => []) const api = fakeApi({ listHosts, requestPairingCode }) const add = mountAddMachine(root, api, { pollMs: 5, now: () => t }) await add.start() t += 100 // advance past expiry await tick(25) // let the poll interval fire and catch expiry expect(root.querySelector('.pair-status')?.textContent).toContain('expired') expect(root.querySelector('.pair-command')?.textContent).toBe('') const callsAtExpiry = listHosts.mock.calls.length await tick(25) expect(listHosts.mock.calls.length).toBe(callsAtExpiry) // polling stopped add.dispose() }) it('a requestPairingCode failure surfaces an error and does not start an infinite poll', async () => { const requestPairingCode = vi.fn(async () => { throw new ApiError('http', 'nope', 500) }) const listHosts = vi.fn(async () => []) const add = mountAddMachine(root, fakeApi({ requestPairingCode, listHosts }), { pollMs: 5 }) await add.start() await flush() expect(root.querySelector('.pair-error')?.textContent).toContain('Could not issue a code') add.dispose() }) it('renders the code via textContent only (no HTML injection)', async () => { const requestPairingCode = vi.fn(async () => ({ code: '', expiresAt: futureIso() })) const add = mountAddMachine(root, fakeApi({ requestPairingCode }), { pollMs: 100000 }) await add.start() const cmd = root.querySelector('.pair-command') as HTMLElement expect(cmd.querySelector('img')).toBeNull() expect(cmd.textContent).toContain('') add.dispose() }) }) function futureIso(): string { return new Date(Date.now() + 120_000).toISOString() }