feat(frontend): adapter web HTTP+WebSocket derrière les ports (#13)

Lot F1 du chantier server/client mode : nouvel adaptateur web branché
derrière les ports d'invocation et de flux live, permettant au frontend de
dialoguer avec le backend via HTTP + WebSocket en mode client/serveur. Le
mode desktop (Tauri IPC) reste inchangé.

- frontend/src/adapters/http : invoker HTTP, client live WebSocket, gateways
  request/response et stream, frames, garde unsupported (7 fichiers + 2 tests).
- frontend/src/app : câblage DI (di.tsx) et son test, typage vite-env.d.ts.

Validé : build vert, garde no-direct-invoke verte, 724 tests verts,
desktop inchangé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 12:50:38 +02:00
parent c8fef2a76a
commit e0cdb4aa56
12 changed files with 1783 additions and 5 deletions

View File

@ -6,7 +6,16 @@ import { describe, it, expect, afterEach, vi } from "vitest";
import { render, screen, renderHook } from "@testing-library/react";
import { MockSystemGateway } from "@/adapters/mock";
import { DIProvider, useGateways, resolveGateways, shouldUseMock } from "./di";
import { HttpSystemGateway } from "@/adapters/http/streamGateways";
import { TauriSystemGateway } from "@/adapters";
import {
DIProvider,
useGateways,
resolveGateways,
resolveTransport,
shouldUseMock,
shouldUseHttp,
} from "./di";
afterEach(() => {
vi.unstubAllEnvs();
@ -26,12 +35,37 @@ describe("shouldUseMock / resolveGateways", () => {
it("returns real (non-mock) gateways otherwise", () => {
vi.stubEnv("VITE_USE_MOCK", "");
vi.stubEnv("VITE_TRANSPORT", "");
expect(shouldUseMock()).toBe(false);
// Real adapter is not the mock implementation.
expect(resolveGateways().system).not.toBeInstanceOf(MockSystemGateway);
});
});
describe("resolveTransport / web (HTTP+WS) selection (ticket #13, F1)", () => {
it("selects the HTTP transport when VITE_TRANSPORT=http", () => {
vi.stubEnv("VITE_USE_MOCK", "");
vi.stubEnv("VITE_TRANSPORT", "http");
expect(shouldUseHttp()).toBe(true);
expect(resolveTransport()).toBe("http");
expect(resolveGateways().system).toBeInstanceOf(HttpSystemGateway);
});
it("keeps Tauri as the default when no transport env is set", () => {
vi.stubEnv("VITE_USE_MOCK", "");
vi.stubEnv("VITE_TRANSPORT", "");
expect(resolveTransport()).toBe("tauri");
expect(resolveGateways().system).toBeInstanceOf(TauriSystemGateway);
});
it("lets mock win over the web transport", () => {
vi.stubEnv("VITE_USE_MOCK", "1");
vi.stubEnv("VITE_TRANSPORT", "http");
expect(resolveTransport()).toBe("mock");
expect(resolveGateways().system).toBeInstanceOf(MockSystemGateway);
});
});
describe("DIProvider / useGateways", () => {
it("provides explicit gateways to consumers", () => {
const gateways = resolveGatewaysMock();

View File

@ -1,9 +1,10 @@
/**
* Dependency-injection provider for the UI ports.
*
* Chooses real Tauri adapters vs mocks based on `VITE_USE_MOCK` (any truthy
* value → mocks). Components consume gateways via {@link useGateways} and never
* construct adapters or call `invoke()` themselves.
* Chooses one of three transport implementations (ticket #13): the mock adapters
* (`VITE_USE_MOCK`), the web HTTP+WS adapters (`VITE_TRANSPORT="http"`), or the
* default Tauri desktop adapters. Components consume gateways via
* {@link useGateways} and never construct adapters or call `invoke()` themselves.
*/
import {
@ -16,18 +17,45 @@ import {
import type { Gateways } from "@/ports";
import { createTauriGateways } from "@/adapters";
import { createMockGateways } from "@/adapters/mock";
import { createHttpWsGateways } from "@/adapters/http";
const GatewaysContext = createContext<Gateways | null>(null);
/** The selected transport backing the gateways. */
export type Transport = "mock" | "http" | "tauri";
/** Whether the mock adapters should be used (env-driven, overridable in tests). */
export function shouldUseMock(): boolean {
const flag = import.meta.env.VITE_USE_MOCK;
return flag === "1" || flag === "true";
}
/**
* Whether the web (client/server) HTTP+WS adapters should be used. Selected
* explicitly via `VITE_TRANSPORT="http"` so the desktop path stays the default
* and is never affected (ticket #13, lot F1).
*/
export function shouldUseHttp(): boolean {
return import.meta.env.VITE_TRANSPORT === "http";
}
/** Resolves which transport to use. Mock wins, then explicit web, else Tauri. */
export function resolveTransport(): Transport {
if (shouldUseMock()) return "mock";
if (shouldUseHttp()) return "http";
return "tauri";
}
/** Resolves the gateway set for the current environment. */
export function resolveGateways(): Gateways {
return shouldUseMock() ? createMockGateways() : createTauriGateways();
switch (resolveTransport()) {
case "mock":
return createMockGateways();
case "http":
return createHttpWsGateways();
case "tauri":
return createTauriGateways();
}
}
interface DIProviderProps {