import { describe, it, expect } from "vitest"; import { ConversationStore } from "../src/conversations/store.js"; import { ChatService } from "../src/agent/chat-service.js"; import type { OllamaClient, OllamaMessage } from "../src/llm/ollama.js"; import type { Guard } from "../src/agent/types.js"; import { createLogger } from "../src/audit/log.js"; const log = createLogger("silent"); const allowGuard: Guard = { authorize: () => ({ allowed: true }) }; describe("ConversationStore", () => { it("crée, ajoute, relit dans l'ordre et liste par récence", () => { const s = new ConversationStore(":memory:"); const a = s.create("api:owner", "Bonjour CHLOVA", 1000); s.append(a, "user", "Bonjour CHLOVA", 1000); s.append(a, "assistant", "Salut", 1001); const b = s.create("api:owner", "Autre sujet", 2000); s.append(b, "user", "Autre sujet", 2000); expect(s.ownerOf(a)).toBe("api:owner"); expect(s.messages(a).map((m) => m.role)).toEqual(["user", "assistant"]); // b plus récent (updated_at) → en tête de liste. expect(s.list("api:owner").map((c) => c.id)).toEqual([b, a]); expect(s.list("api:owner")[0]!.title).toBe("Autre sujet"); s.close(); }); it("recent() borne et conserve l'ordre chronologique", () => { const s = new ConversationStore(":memory:"); const c = s.create("u", "x", 0); for (let i = 0; i < 30; i++) s.append(c, i % 2 ? "assistant" : "user", `m${i}`, i); const recent = s.recent(c, 5); expect(recent).toHaveLength(5); expect(recent.map((m) => m.content)).toEqual(["m25", "m26", "m27", "m28", "m29"]); s.close(); }); it("isole les conversations par acteur", () => { const s = new ConversationStore(":memory:"); s.create("a", "x"); s.create("b", "y"); expect(s.list("a")).toHaveLength(1); expect(s.list("b")).toHaveLength(1); s.close(); }); }); describe("ChatService mémoire multi-tour", () => { // Client factice : snapshot (copie) des messages AU MOMENT de l'appel — le // tableau réel est muté ensuite par la boucle (push assistant), donc on copie. let lastLen = 0; let lastContents: string[] = []; const client = { chat: async (req: { messages: OllamaMessage[] }) => { lastLen = req.messages.length; lastContents = req.messages.map((m) => m.content); return { role: "assistant", content: "ok" } as OllamaMessage; }, } as unknown as OllamaClient; it("persiste et rejoue l'historique au tour suivant", async () => { const store = new ConversationStore(":memory:"); const svc = new ChatService({ client, tools: [], guard: allowGuard, systemPrompt: "SYS", logger: log, store, }); const r1 = await svc.handle("api:owner", "premier"); expect(r1.conversationId).toBeTruthy(); // Tour 1 : system + user = 2 messages, pas d'historique. expect(lastLen).toBe(2); const r2 = await svc.handle("api:owner", "second", r1.conversationId!); expect(r2.conversationId).toBe(r1.conversationId); // Tour 2 : system + (user1, assistant1) + user2 = 4 messages. expect(lastLen).toBe(4); expect(lastContents).toEqual(["SYS", "premier", "ok", "second"]); // 4 messages persistés (2 user + 2 assistant). expect(store.messages(r1.conversationId!)).toHaveLength(4); store.close(); }); it("refuse une conversation d'un autre acteur", async () => { const store = new ConversationStore(":memory:"); const svc = new ChatService({ client, tools: [], guard: allowGuard, systemPrompt: "S", logger: log, store }); const r = await svc.handle("alice", "coucou"); await expect(svc.handle("bob", "intrus", r.conversationId!)).rejects.toThrow(); store.close(); }); });