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>
130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|