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); }); it("commande shell : détail, échec signalé, longue sortie tronquée", () => { const s = new UIStore(); s.shell("ls", "a\nb", null); s.shell("false", "", "code de sortie : 1"); s.shell("cat gros", "x".repeat(10_000), null); const [ok, ko, big] = s.getSnapshot().items; expect(ok).toMatchObject({ kind: "shell", text: "ls", detail: "a\nb", failed: false }); expect(ko).toMatchObject({ detail: "(aucune sortie)\n[code de sortie : 1]", failed: true }); expect(big?.detail).toContain("caractères omis à l'affichage"); }); });