test(frontend): Playwright e2e F1 vs backend+fake-pi (hermetic)

Adds a real-browser Playwright e2e of the full F1 disambiguation loop,
driven against the real backend + fake binaries (no VPN, no real Pi, no Python).

- frontend/e2e/fixtures/fake-tht.mjs: stubs tht CLI (session new/list/show)
- frontend/e2e/fixtures/fake-pi.mjs: wraps harness fake-pi-rpc with f1_disambiguation.json
- frontend/playwright.config.ts: two webServer entries (backend:8799, frontend:5199)
- frontend/e2e/f1.spec.ts: open app → create session → wait for SelectWidget → respond

Bug fixes discovered during e2e:
- useSessionStream: add addEventListener for named SSE events (backend sends
  'event: ui_request' etc.; onmessage only fires for unnamed 'event: message')
- FakeEventSource: add no-op addEventListener/removeEventListener stubs
- backend SSE route: add CORS headers manually in writeHead() since
  reply.raw bypasses the @fastify/cors onSend hook
- backend: install and register @fastify/cors for all non-SSE routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 13:27:00 +02:00
co-authored by Claude Sonnet 4.6
parent 3dec4c3af0
commit 1171181f9a
13 changed files with 292 additions and 4 deletions
+9 -1
View File
@@ -13,7 +13,8 @@ export function useSessionStream(sessionId: string | null) {
const es = new EventSource(`${BASE}/sessions/${sessionId}/events`);
es.onopen = () => setConnected(true);
es.onerror = () => setConnected(false);
es.onmessage = (ev) => {
const handle = (ev: MessageEvent) => {
try {
applyEvent(JSON.parse(ev.data) as StreamEvent);
} catch {
@@ -21,7 +22,14 @@ export function useSessionStream(sessionId: string | null) {
}
};
// onmessage catches unnamed events; addEventListener catches named events
// (the backend sends event: ui_request, event: text_delta, etc.)
es.onmessage = handle;
const namedEvents = ["ui_request", "text_delta", "info", "system_event"] as const;
for (const name of namedEvents) es.addEventListener(name, handle);
return () => {
for (const name of namedEvents) es.removeEventListener(name, handle);
es.close();
setConnected(false);
};