0da5e2aba1
Store SQLite conversations/messages (propriété par actor, fenêtre 20), historique rejoué au LLM (runAgentTurn history), ChatService persiste et renvoie conversationId. API GET/DELETE /conversations + chat avec conversationId. UI Chat: sidebar conversations (drawer mobile), nouvelle, reprise, suppression. docs/conversations.md. 83 tests verts, build web vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016w5jRe87MGdd6AMvXQcHNi
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
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();
|
|
});
|
|
});
|