import { describe, expect, it } from "vitest"; import { createSession, runTurn } from "./agent"; import type { AgentUI, LLMProvider, PermissionPolicy, StreamEvent, Tool } from "./types"; const silentUI: AgentUI = { onThinking() {}, onText() {}, onTextEnd() {}, onToolCall() {}, onToolResult() {}, confirm: async () => true, }; /** Provider factice : 1er appel → demande d'outil, 2e appel → réponse texte. */ function fakeProvider(toolName = "echo"): LLMProvider { let call = 0; return { id: "fake", model: "fake", capabilities: { tools: true }, async *chat(): AsyncIterable { call++; if (call === 1) { yield { type: "tool_use", id: "t1", name: toolName, input: { value: "salut" } }; yield { type: "done", stopReason: "tool_use" }; } else { yield { type: "text_delta", text: "terminé" }; yield { type: "done", stopReason: "end_turn" }; } }, }; } describe("runTurn", () => { it("exécute l'outil demandé puis renvoie la réponse finale", async () => { const echo: Tool = { spec: { name: "echo", description: "echo", inputSchema: { type: "object" } }, permission: "auto", execute: async (input) => String(input.value), }; const session = createSession({ provider: fakeProvider(), init: { systemPrompt: "test", tools: [echo] }, ui: silentUI, cwd: process.cwd(), }); await runTurn(session, "bonjour"); // user, assistant(tool_use), user(tool_result), assistant(texte) expect(session.messages).toHaveLength(4); expect(session.messages[2]?.content[0]).toMatchObject({ type: "tool_result", content: "salut", }); }); }); describe("politique de permissions", () => { const makeTool = (ran: string[]): Tool => ({ spec: { name: "danger", description: "d", inputSchema: { type: "object" } }, permission: "ask", execute: async () => { ran.push("x"); return "ok"; }, }); const policy = (decision: "allow" | "ask" | "deny"): PermissionPolicy => ({ check: () => ({ decision, reason: "règle de test" }), }); async function run(decision: "allow" | "ask" | "deny", answer: boolean) { const ran: string[] = []; const confirms: string[] = []; const session = createSession({ provider: fakeProvider("danger"), init: { systemPrompt: "t", tools: [makeTool(ran)], permissions: policy(decision) }, ui: { ...silentUI, confirm: async (m) => (confirms.push(m), answer) }, cwd: process.cwd(), }); await runTurn(session, "go"); return { ran, confirms, session }; } it("allow : exécute sans confirmation", async () => { const { ran, confirms } = await run("allow", false); expect(ran).toHaveLength(1); expect(confirms).toHaveLength(0); }); it("deny : refuse sans demander", async () => { const { ran, confirms, session } = await run("deny", true); expect(ran).toHaveLength(0); expect(confirms).toHaveLength(0); expect(session.messages[2]?.content[0]).toMatchObject({ type: "tool_result", isError: true }); }); it("ask : demande confirmation même pour un outil automatique", async () => { const { ran, confirms } = await run("ask", true); expect(confirms).toHaveLength(1); expect(ran).toHaveLength(1); }); }); describe("contexte en attente", () => { it("est joint à la prochaine demande puis vidé", async () => { const session = createSession({ provider: fakeProvider("echo"), init: { systemPrompt: "t", tools: [{ spec: { name: "echo", description: "e", inputSchema: { type: "object" } }, permission: "auto", execute: async () => "ok" }], }, ui: silentUI, cwd: process.cwd(), }); session.pendingContext.push("CONTEXTE-A", "CONTEXTE-B"); await runTurn(session, "ma demande"); const first = session.messages[0]?.content[0]; expect(first).toMatchObject({ type: "text", text: "CONTEXTE-A\n\nCONTEXTE-B\n\nma demande" }); expect(session.pendingContext).toEqual([]); }); });