/** * F5 — the live web surfaces: a `event.domain` event refreshes the displayed * work-state; background tasks render with status + cancel/retry wired to the * gateway; a WS reconnect re-synchronises the read-model. */ import { describe, it, expect, vi, afterEach } from "vitest"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import type { Gateways } from "@/ports"; import type { BackgroundCompletion, ProjectWorkState } from "@/domain"; import { DIProvider } from "@/app/di"; import { createMockGateways, MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock"; import { WsLiveClient, setWebLiveClient, WebSession, } from "@/adapters/http"; import { WebApp } from "./WebApp"; afterEach(() => setWebLiveClient(null)); const okFetch = async () => ({ ok: true, status: 200, json: async () => ({ ok: true }), text: async () => "{}", }); function memStore() { const map = new Map(); return { getItem: (k: string) => map.get(k) ?? null, setItem: (k: string, v: string) => void map.set(k, v), removeItem: (k: string) => void map.delete(k), }; } function state(agents: ProjectWorkState["agents"]): ProjectWorkState { return { agents, conversations: [] }; } async function seeded(initial: ProjectWorkState): Promise<{ gateways: Gateways; projectId: string }> { const gateways = createMockGateways(); const project = await gateways.project.createProject("Demo", "/srv/demo"); (gateways.workState as MockWorkStateGateway)._setProjectWorkState(project.id, initial); return { gateways, projectId: project.id }; } function renderPaired(gateways: Gateways) { const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() }); session.markPaired(); return render( , ); } const AGENT_IDLE = { agentId: "a1", name: "Archi", profileId: "p1", busy: { state: "idle" as const }, tickets: [] }; describe("WebWorkspace live surfaces (F5)", () => { it("refreshes the work-state on a relevant event.domain event", async () => { const { gateways, projectId } = await seeded(state([AGENT_IDLE])); renderPaired(gateways); fireEvent.click(await screen.findByText("Demo")); const panel = await screen.findByTestId("web-workstate"); expect(panel.textContent).toContain("Idle"); // The agent goes busy server-side; the read-model now reflects it… (gateways.workState as MockWorkStateGateway)._setProjectWorkState( projectId, state([{ ...AGENT_IDLE, busy: { state: "busy", ticket: "t-1", sinceMs: 1 } }]), ); // …and an `agentBusyChanged` domain event drives a live refresh. act(() => { (gateways.system as MockSystemGateway).emit({ type: "agentBusyChanged", agentId: "a1", busy: true }); }); await waitFor(() => expect(screen.getByTestId("web-workstate").textContent).toContain("Busy"), ); }); it("renders background tasks and wires cancel through the gateway", async () => { const task: BackgroundCompletion = { taskId: "bt-1", ownerAgentId: "a1", projectId: "p", kind: "shell", status: "running", exitCode: null, summary: null, stdoutTail: null, stderrTail: null, updatedAtMs: 1, }; const { gateways } = await seeded(state([{ ...AGENT_IDLE, backgroundTasks: [task] }])); const cancelSpy = vi.spyOn(gateways.workState, "cancelBackgroundTask"); renderPaired(gateways); fireEvent.click(await screen.findByText("Demo")); const tasks = await screen.findByLabelText("Archi background tasks"); expect(tasks.textContent).toContain("Running"); expect(tasks.textContent).toContain("shell"); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); await waitFor(() => expect(cancelSpy).toHaveBeenCalledWith("bt-1")); }); it("re-synchronises the read-model when the WS reconnects", async () => { const { gateways, projectId } = await seeded(state([AGENT_IDLE])); // Register a live client whose connection state we can drive. let notify: ((s: "connecting" | "connected" | "reconnecting" | "closed") => void) | null = null; const fakeClient = { getConnectionState: () => "connected" as const, onConnectionStateChange: (l: (s: "connecting" | "connected" | "reconnecting" | "closed") => void) => { notify = l; return () => { notify = null; }; }, }; setWebLiveClient(fakeClient as unknown as WsLiveClient); const refreshSpy = vi.spyOn(gateways.workState, "getProjectWorkState"); renderPaired(gateways); fireEvent.click(await screen.findByText("Demo")); await screen.findByTestId("web-workstate"); const callsAfterOpen = refreshSpy.mock.calls.length; expect(projectId).toBeTruthy(); // Simulate a reconnect: reconnecting → connected must re-fetch the read-model. act(() => { notify?.("reconnecting"); notify?.("connected"); }); await waitFor(() => expect(refreshSpy.mock.calls.length).toBeGreaterThan(callsAfterOpen)); }); });