feat(frontend): client web read-only pairing + snapshot état (#13)

Lot F2 du chantier server/client mode : client web read-only complétant le
premier incrément livrable — pairing, liste des projets, ouverture et
snapshot de l'état, sans PTY.

- frontend/src/adapters/http/webSession.ts : session web (pairing/cookie).
- frontend/src/features/web : PairingScreen, WebWorkspace, WebApp, index.
- Câblage main.tsx et adaptations httpInvoker.ts / index.ts (cas 401).
- Tests : webSession.test.ts, WebApp.test.tsx, cas 401 dans
  httpInvoker.test.ts.

Validé : frontend 736 tests verts, build vert, garde no-direct-invoke
verte, contrat B4↔F2 aligné, desktop non régressé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 13:21:35 +02:00
parent fa353f6c0b
commit e500e31663
11 changed files with 768 additions and 3 deletions

View File

@ -77,6 +77,29 @@ describe("HttpInvoker", () => {
});
});
it("fires onUnauthorized on a 401 (and still throws)", async () => {
const { fetchImpl } = fakeFetch(
{ code: "UNAUTHENTICATED", message: "no session" },
{ ok: false, status: 401 },
);
const onUnauthorized = vi.fn();
const http = new HttpInvoker({ baseUrl: "https://host", fetchImpl, onUnauthorized });
await expect(http.invoke("list_projects")).rejects.toMatchObject({
code: "UNAUTHENTICATED",
});
expect(onUnauthorized).toHaveBeenCalledOnce();
});
it("does not fire onUnauthorized on a non-401 error", async () => {
const { fetchImpl } = fakeFetch({ code: "NOT_FOUND", message: "x" }, { ok: false, status: 404 });
const onUnauthorized = vi.fn();
const http = new HttpInvoker({ baseUrl: "https://host", fetchImpl, onUnauthorized });
await expect(http.invoke("open_project", { projectId: "x" })).rejects.toBeTruthy();
expect(onUnauthorized).not.toHaveBeenCalled();
});
it("wraps a network failure in a TRANSPORT_ERROR GatewayError", async () => {
const fetchImpl: FetchLike = async () => {
throw new Error("boom");