104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
|
|
import { describe, expect, it } from "vitest";
|
||
|
|
import { UIStore } from "./store";
|
||
|
|
|
||
|
|
const kinds = (s: UIStore) => s.getSnapshot().items.map((i) => `${i.kind}:${i.text}`);
|
||
|
|
|
||
|
|
describe("UIStore", () => {
|
||
|
|
it("le texte streamé devient un message à la fin de la réponse", () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
s.appendStreaming("Bon");
|
||
|
|
s.appendStreaming("jour\n");
|
||
|
|
expect(s.getSnapshot().streaming).toBe("Bonjour\n");
|
||
|
|
s.endStreaming();
|
||
|
|
expect(s.getSnapshot().streaming).toBe("");
|
||
|
|
expect(kinds(s)).toEqual(["assistant:Bonjour"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("notifie les abonnés et fournit un instantané stable entre deux changements", () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
let calls = 0;
|
||
|
|
const off = s.subscribe(() => calls++);
|
||
|
|
const before = s.getSnapshot();
|
||
|
|
expect(s.getSnapshot()).toBe(before);
|
||
|
|
s.log("info", "a");
|
||
|
|
expect(calls).toBe(1);
|
||
|
|
expect(s.getSnapshot()).not.toBe(before);
|
||
|
|
off();
|
||
|
|
s.log("info", "b");
|
||
|
|
expect(calls).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("readInput : la saisie est journalisée, annulée = null, l'historique est transmis", async () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
|
||
|
|
const first = s.readInput();
|
||
|
|
const p1 = s.getSnapshot().prompt;
|
||
|
|
expect(p1?.kind).toBe("input");
|
||
|
|
(p1 as { resolve(v: string | null): void }).resolve("bonjour");
|
||
|
|
expect(await first).toBe("bonjour");
|
||
|
|
expect(s.getSnapshot().prompt).toBeNull();
|
||
|
|
expect(kinds(s)).toEqual(["user:bonjour"]);
|
||
|
|
|
||
|
|
const second = s.readInput();
|
||
|
|
const p2 = s.getSnapshot().prompt as { history: string[]; resolve(v: string | null): void };
|
||
|
|
expect(p2.history).toEqual(["bonjour"]);
|
||
|
|
p2.resolve(null);
|
||
|
|
expect(await second).toBeNull();
|
||
|
|
expect(kinds(s)).toEqual(["user:bonjour"]); // rien de journalisé à l'annulation
|
||
|
|
});
|
||
|
|
|
||
|
|
it("select : présélection, valeur renvoyée, choix journalisé", async () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
const choices = [
|
||
|
|
{ value: "a", label: "Alpha" },
|
||
|
|
{ value: "b", label: "Bravo" },
|
||
|
|
];
|
||
|
|
const result = s.select("Modèle", choices, "b");
|
||
|
|
const prompt = s.getSnapshot().prompt as { kind: string; initialIndex: number; resolve(v: unknown): void };
|
||
|
|
expect(prompt.kind).toBe("select");
|
||
|
|
expect(prompt.initialIndex).toBe(1);
|
||
|
|
prompt.resolve("a");
|
||
|
|
expect(await result).toBe("a");
|
||
|
|
expect(kinds(s)).toEqual(["choice:Modèle : Alpha"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("select : annulation = null, liste vide = null sans invite", async () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
const cancelled = s.select("Modèle", [{ value: 1, label: "un" }]);
|
||
|
|
(s.getSnapshot().prompt as { resolve(v: unknown): void }).resolve(null);
|
||
|
|
expect(await cancelled).toBeNull();
|
||
|
|
expect(await s.select("Vide", [])).toBeNull();
|
||
|
|
expect(s.getSnapshot().prompt).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("confirm", async () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
const answer = s.confirm("Autoriser Bash ?");
|
||
|
|
(s.getSnapshot().prompt as { resolve(v: boolean): void }).resolve(false);
|
||
|
|
expect(await answer).toBe(false);
|
||
|
|
expect(s.getSnapshot().prompt).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("résultats d'outils : tronqués, erreurs distinguées, vides ignorés", () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
s.toolCall("Read", "a.ts");
|
||
|
|
s.toolResult("Read", "x".repeat(1000), false);
|
||
|
|
s.toolResult("Bash", "", false);
|
||
|
|
s.toolResult("Bash", "échec", true);
|
||
|
|
const items = s.getSnapshot().items;
|
||
|
|
expect(items.map((i) => i.kind)).toEqual(["step", "result", "error"]);
|
||
|
|
expect(items[1]?.text.endsWith("… (tronqué)")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("interrupt : false sans gestionnaire, sinon l'appelle", () => {
|
||
|
|
const s = new UIStore();
|
||
|
|
expect(s.interrupt()).toBe(false);
|
||
|
|
let called = 0;
|
||
|
|
s.setInterruptHandler(() => called++);
|
||
|
|
expect(s.interrupt()).toBe(true);
|
||
|
|
expect(called).toBe(1);
|
||
|
|
s.setInterruptHandler(null);
|
||
|
|
expect(s.interrupt()).toBe(false);
|
||
|
|
});
|
||
|
|
});
|