feat(model-server): frontend modèles locaux — badge de statut & CRUD serveurs (#35) et wizard multi-profils OpenCode (#36)
Sprint « Modeles locaux », couche frontend. #35 : - F35.1 badge de statut de lancement du serveur local (ModelServerLaunchBadge + useAgentsModelServer). - F35.2 feature model-servers : CRUD (ModelServersPanel / useModelServers / gateway modelServer) et ModelServerSelect. #36 : - Liste multi-profils OpenCode dans le wizard de premier lancement, gateway de clonage (clone_opencode_profile_from_seed). Tests verts (exécution réelle) : tsc propre, vitest 608/608. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
129
frontend/src/features/agents/useAgentsModelServer.test.tsx
Normal file
129
frontend/src/features/agents/useAgentsModelServer.test.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* F35.1 — local model server launch state in {@link useAgents}.
|
||||
*
|
||||
* Drives the hook behind the real {@link DIProvider} with an in-memory
|
||||
* {@link MockSystemGateway} to emit the two F35 domain events, and verifies they
|
||||
* fold into `modelServerStatusByServer` (keyed by serverId) and
|
||||
* `launchFailureByAgent` (keyed by agentId).
|
||||
*
|
||||
* Cases:
|
||||
* - `modelServerStatusChanged` → status by server id (probing → starting → ready)
|
||||
* - `agentLaunchFailed` → actionable failure by agent id
|
||||
* - `agentLaunched` clears a prior launch failure for that agent
|
||||
* - a `failed` status is retained per server (independent of the agent map)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
|
||||
import type { DomainEvent } from "@/domain";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { MockSystemGateway } from "@/adapters/mock";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { useAgents } from "./useAgents";
|
||||
|
||||
const PROJECT_ID = "proj-modelserver-001";
|
||||
const AGENT = "agent-oc";
|
||||
const SERVER = "srv-42";
|
||||
|
||||
function setup() {
|
||||
const system = new MockSystemGateway();
|
||||
const agent = {
|
||||
listAgents: async () => [],
|
||||
listLiveAgents: async () => [],
|
||||
};
|
||||
const profile = { listProfiles: async () => [] };
|
||||
const gateways = { system, agent, profile } as unknown as Gateways;
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<DIProvider gateways={gateways}>{children}</DIProvider>
|
||||
);
|
||||
const view = renderHook(() => useAgents(PROJECT_ID), { wrapper });
|
||||
return { system, view };
|
||||
}
|
||||
|
||||
async function emit(system: MockSystemGateway, event: DomainEvent) {
|
||||
await act(async () => {
|
||||
system.emit(event);
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe("useAgents — local model server launch state (F35.1)", () => {
|
||||
it("folds modelServerStatusChanged into modelServerStatusByServer by serverId", async () => {
|
||||
const { system, view } = setup();
|
||||
|
||||
await emit(system, {
|
||||
type: "modelServerStatusChanged",
|
||||
serverId: SERVER,
|
||||
status: { state: "probing" },
|
||||
});
|
||||
expect(view.result.current.modelServerStatusByServer[SERVER]).toEqual({
|
||||
state: "probing",
|
||||
});
|
||||
|
||||
await emit(system, {
|
||||
type: "modelServerStatusChanged",
|
||||
serverId: SERVER,
|
||||
status: { state: "starting" },
|
||||
});
|
||||
await emit(system, {
|
||||
type: "modelServerStatusChanged",
|
||||
serverId: SERVER,
|
||||
status: { state: "ready", reused: true },
|
||||
});
|
||||
expect(view.result.current.modelServerStatusByServer[SERVER]).toEqual({
|
||||
state: "ready",
|
||||
reused: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("folds agentLaunchFailed into launchFailureByAgent by agentId", async () => {
|
||||
const { system, view } = setup();
|
||||
await emit(system, {
|
||||
type: "agentLaunchFailed",
|
||||
agentId: AGENT,
|
||||
cause: "model_server",
|
||||
code: "SERVER_UNREACHABLE",
|
||||
message: "llama-server did not become ready on :8080",
|
||||
});
|
||||
expect(view.result.current.launchFailureByAgent[AGENT]).toEqual({
|
||||
cause: "model_server",
|
||||
code: "SERVER_UNREACHABLE",
|
||||
message: "llama-server did not become ready on :8080",
|
||||
});
|
||||
});
|
||||
|
||||
it("a successful agentLaunched clears the agent's prior launch failure", async () => {
|
||||
const { system, view } = setup();
|
||||
await emit(system, {
|
||||
type: "agentLaunchFailed",
|
||||
agentId: AGENT,
|
||||
cause: "model_server",
|
||||
code: "SERVER_UNREACHABLE",
|
||||
message: "boom",
|
||||
});
|
||||
expect(view.result.current.launchFailureByAgent[AGENT]).toBeTruthy();
|
||||
|
||||
await emit(system, {
|
||||
type: "agentLaunched",
|
||||
agentId: AGENT,
|
||||
sessionId: "sess-1",
|
||||
});
|
||||
expect(view.result.current.launchFailureByAgent[AGENT]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps a per-server failed status independent of the agent failure map", async () => {
|
||||
const { system, view } = setup();
|
||||
await emit(system, {
|
||||
type: "modelServerStatusChanged",
|
||||
serverId: SERVER,
|
||||
status: { state: "failed", code: "SPAWN", message: "binary not found" },
|
||||
});
|
||||
expect(view.result.current.modelServerStatusByServer[SERVER]).toEqual({
|
||||
state: "failed",
|
||||
code: "SPAWN",
|
||||
message: "binary not found",
|
||||
});
|
||||
expect(view.result.current.launchFailureByAgent[AGENT]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user