import { describe, expect, it, vi } from 'vitest' import { deriveContentKey, openReplayCiphertext, sealReplayFrame } from 'relay-e2e' import { randomBytes } from 'relay-e2e' import { mountPreviewClient, type PreviewDeps, type ReadonlyTerminalLike, type ReplaySource, } from '../src/preview-client' const HOST_SECRET = randomBytes(32) const SESSION_ID = 'sess-1' const ALG = 'aes-256-gcm' as const /** Seal a screen (as a P2 agent stub would) under K_content with sealReplayFrame. */ function sealedReplay(lines: readonly string[], secret = HOST_SECRET): ReplaySource { const k = deriveContentKey({ hostContentSecret: secret, sessionId: SESSION_ID, alg: ALG }) const frames = lines.map((line, i) => // sealReplayFrame → E2EEnvelope, encoded to a DATA payload the browser opens. // openReplayCiphertext decodes+opens; we mirror the encode via the session envelope codec. encodeReplayFrame(sealReplayFrame(k, BigInt(i), new TextEncoder().encode(line))), ) return { sessionId: SESSION_ID, alg: ALG, frames } } // The replay codec: relay-e2e's openReplayCiphertext expects the encoded envelope bytes. import { encodeEnvelope } from 'relay-e2e' function encodeReplayFrame(env: ReturnType): Uint8Array { return encodeEnvelope(env) } const realDeps: PreviewDeps = { deriveContentKey, openReplayCiphertext } function mockTerminal(): ReadonlyTerminalLike & { written: string[] } { const written: string[] = [] return { written, open: () => {}, write: (d) => written.push(d), dispose: () => {} } } describe('mountPreviewClient (T9)', () => { it('recoverable-key decrypt: sealReplayFrame frames render into the read-only xterm (FIX 3)', async () => { const card = document.createElement('div') const term = mockTerminal() const client = mountPreviewClient( card, sealedReplay(['line-A', 'line-B']), HOST_SECRET, { cols: 80, rows: 24 }, { ...realDeps, createTerminal: () => term }, ) await client.render() expect(term.written).toEqual(['line-A', 'line-B']) }) it('wrong/host-mismatched secret → openReplayCiphertext throws → "unavailable", never garbled', async () => { const card = document.createElement('div') const term = mockTerminal() const replay = sealedReplay(['secret-screen']) // sealed under HOST_SECRET const client = mountPreviewClient( card, replay, randomBytes(32), // a DIFFERENT host secret → derives a different K_content { cols: 80, rows: 24 }, { ...realDeps, createTerminal: () => term }, ) await client.render() expect(card.querySelector('.preview-unavailable')?.textContent).toBe('unavailable') expect(term.written).toEqual([]) // never wrote a torn screen }) it('read-only: the preview terminal exposes NO input path (no onData/send)', async () => { const card = document.createElement('div') const term = mockTerminal() mountPreviewClient( card, sealedReplay(['x']), HOST_SECRET, { cols: 80, rows: 24 }, { ...realDeps, createTerminal: () => term }, ).render() expect('onData' in term).toBe(false) expect('send' in term).toBe(false) }) it('never writes hostContentSecret to localStorage/console', async () => { const spy = vi.spyOn(console, 'log').mockImplementation(() => {}) const card = document.createElement('div') await mountPreviewClient( card, sealedReplay(['x']), HOST_SECRET, { cols: 80, rows: 24 }, { ...realDeps, createTerminal: () => mockTerminal() }, ).render() expect(spy).not.toHaveBeenCalled() expect(JSON.stringify(localStorage)).not.toContain( Array.from(HOST_SECRET.slice(0, 4)).join(','), ) spy.mockRestore() }) })