Lot F5 du chantier server/client mode : le client web consomme les frames event.domain (B7) pour animer ses surfaces live. - webLive.ts : consommation du flux event.domain (workstate, background, inbox). - useLiveReconnect.ts : reconnexion du flux live. - wsLiveClient.ts / index.ts : câblage du transport live. - WebWorkspace.tsx : surfaces live branchées. - Tests : WebWorkspaceLive.test.tsx, wsLiveClientReconnect.test.ts, WebApp.test.tsx. Validé : frontend 753 tests verts, desktop non régressé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
5.2 KiB
TypeScript
136 lines
5.2 KiB
TypeScript
/**
|
|
* F2 — the web client routing + pairing flow:
|
|
* - not paired ⇒ pairing screen; wrong code ⇒ clear error, stays on pairing;
|
|
* - successful pair ⇒ read-only workspace (project list + open + snapshot);
|
|
* - a 401 (session.notifyUnauthorized) ⇒ back to pairing.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
|
import type { Gateways } from "@/ports";
|
|
import { DIProvider } from "@/app/di";
|
|
import { createMockGateways, MockWorkStateGateway } from "@/adapters/mock";
|
|
import { WebSession } from "@/adapters/http";
|
|
import type { FetchLike } from "@/adapters/http/httpInvoker";
|
|
import type { FlagStore } from "@/adapters/http/webSession";
|
|
import { WebApp } from "./WebApp";
|
|
|
|
function memStore(): FlagStore {
|
|
const map = new Map<string, string>();
|
|
return {
|
|
getItem: (k) => map.get(k) ?? null,
|
|
setItem: (k, v) => void map.set(k, v),
|
|
removeItem: (k) => void map.delete(k),
|
|
};
|
|
}
|
|
|
|
const okFetch: FetchLike = async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ ok: true }),
|
|
text: async () => "{}",
|
|
});
|
|
|
|
const rejectFetch: FetchLike = async () => ({
|
|
ok: false,
|
|
status: 401,
|
|
json: async () => ({ code: "INVALID", message: "bad" }),
|
|
text: async () => "{}",
|
|
});
|
|
|
|
/** Mock gateways seeded with one project + a work-state snapshot. */
|
|
async function seededGateways(): Promise<Gateways> {
|
|
const gateways = createMockGateways();
|
|
const project = await gateways.project.createProject("Demo", "/srv/demo");
|
|
(gateways.workState as MockWorkStateGateway)._setProjectWorkState(project.id, {
|
|
agents: [
|
|
{ agentId: "a1", name: "Archi", profileId: "p1", busy: { state: "idle" }, tickets: [] },
|
|
],
|
|
conversations: [],
|
|
});
|
|
return gateways;
|
|
}
|
|
|
|
function renderWebApp(session: WebSession, gateways: Gateways) {
|
|
return render(
|
|
<DIProvider gateways={gateways}>
|
|
<WebApp session={session} />
|
|
</DIProvider>,
|
|
);
|
|
}
|
|
|
|
describe("WebApp pairing routing", () => {
|
|
it("shows the pairing screen when not paired", async () => {
|
|
const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() });
|
|
renderWebApp(session, await seededGateways());
|
|
|
|
expect(screen.getByText("Appairer cet appareil")).toBeTruthy();
|
|
expect(screen.queryByText("Projets")).toBeNull();
|
|
});
|
|
|
|
it("advances to the read-only workspace after a successful pair", async () => {
|
|
const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() });
|
|
renderWebApp(session, await seededGateways());
|
|
|
|
fireEvent.change(screen.getByLabelText("Code d'appairage"), { target: { value: "4821-93" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Appairer" }));
|
|
|
|
expect(await screen.findByText("Projets")).toBeTruthy();
|
|
expect(await screen.findByText("Demo")).toBeTruthy();
|
|
expect(session.isPaired()).toBe(true);
|
|
});
|
|
|
|
it("shows a clear error on a wrong code and stays on pairing", async () => {
|
|
const session = new WebSession({ baseUrl: "https://h", fetchImpl: rejectFetch, store: memStore() });
|
|
renderWebApp(session, await seededGateways());
|
|
|
|
fireEvent.change(screen.getByLabelText("Code d'appairage"), { target: { value: "nope" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Appairer" }));
|
|
|
|
expect(await screen.findByTestId("pairing-error")).toBeTruthy();
|
|
expect(screen.queryByText("Projets")).toBeNull();
|
|
expect(session.isPaired()).toBe(false);
|
|
});
|
|
|
|
it("opens a project read-only and renders the work-state snapshot", async () => {
|
|
const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() });
|
|
session.markPaired();
|
|
renderWebApp(session, await seededGateways());
|
|
|
|
// Workspace is shown directly (already paired); open the seeded project.
|
|
fireEvent.click(await screen.findByText("Demo"));
|
|
|
|
const snapshot = await screen.findByTestId("web-workstate");
|
|
expect(snapshot.textContent).toContain("Archi");
|
|
expect(snapshot.textContent).toContain("Idle");
|
|
});
|
|
|
|
it("opens a live agent cell for a work-state agent (F4 affordance)", async () => {
|
|
const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() });
|
|
session.markPaired();
|
|
renderWebApp(session, await seededGateways());
|
|
|
|
fireEvent.click(await screen.findByText("Demo"));
|
|
await screen.findByTestId("web-workstate");
|
|
|
|
// The per-agent "Ouvrir" affordance mounts the reusable TerminalView cell,
|
|
// wired to the DI agent gateway (the CLI runs server-side).
|
|
expect(screen.queryByTestId("web-agent-cell")).toBeNull();
|
|
fireEvent.click(screen.getByRole("button", { name: "Ouvrir" }));
|
|
expect(await screen.findByTestId("web-agent-cell")).toBeTruthy();
|
|
});
|
|
|
|
it("returns to pairing when the session reports unauthorized (401)", async () => {
|
|
const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() });
|
|
session.markPaired();
|
|
renderWebApp(session, await seededGateways());
|
|
|
|
expect(await screen.findByText("Projets")).toBeTruthy();
|
|
|
|
act(() => session.notifyUnauthorized());
|
|
|
|
await waitFor(() => expect(screen.getByText("Appairer cet appareil")).toBeTruthy());
|
|
expect(screen.queryByText("Projets")).toBeNull();
|
|
});
|
|
});
|