104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
|
|
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<StreamEvent> {
|
||
|
|
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);
|
||
|
|
});
|
||
|
|
});
|