Files
chlova/orchestrator/test/api.test.ts
T
Kantin-Petit 0da5e2aba1 feat(chat): conversations persistantes + mémoire multi-tour (v0.36.0)
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
2026-06-23 23:25:42 +02:00

103 lines
3.6 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import Fastify, { type FastifyInstance } from "fastify";
import { generateSync, generateSecret } from "otplib";
import { registerApi } from "../src/api/routes.js";
import { hashPassword, type AuthConfig } from "../src/api/auth.js";
import { ChatService } from "../src/agent/chat-service.js";
import { ReviewService } from "../src/gatekeeper/review.js";
import { AssetRepository } from "../src/gatekeeper/repository.js";
import { createAsset } from "../src/gatekeeper/assets.js";
import { createLogger } from "../src/audit/log.js";
const log = createLogger("silent");
const auth: AuthConfig = {
adminUser: "kantin",
adminPasswordHash: hashPassword("pw"),
totpSecret: generateSecret(),
jwtSecret: "jwt-secret-suffisamment-long-pour-hs256",
};
// ChatService stub : pas de vrai LLM.
const chatStub = {
handle: async (_actor: string, text: string) => ({ reply: `echo:${text}`, conversationId: null }),
} as unknown as ChatService;
let app: FastifyInstance;
let repo: AssetRepository;
beforeEach(async () => {
repo = new AssetRepository(":memory:");
const review = new ReviewService(repo, log);
app = Fastify();
await registerApi(app, { auth, chat: chatStub, review, conversations: null, state: () => ({ phase: "2-write-review", tools: 3 }) });
await app.ready();
});
afterEach(async () => {
await app.close();
repo.close();
});
async function tokenOf(): Promise<string> {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { user: "kantin", password: "pw", totp: generateSync({ secret: auth.totpSecret }) },
});
return res.json().token as string;
}
describe("API auth", () => {
it("login renvoie un token avec bons facteurs", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { user: "kantin", password: "pw", totp: generateSync({ secret: auth.totpSecret }) },
});
expect(res.statusCode).toBe(200);
expect(res.json().token).toBeTruthy();
});
it("login 401 avec mauvais mot de passe", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { user: "kantin", password: "bad", totp: generateSync({ secret: auth.totpSecret }) },
});
expect(res.statusCode).toBe(401);
});
it("/api/chat refuse sans token (401)", async () => {
const res = await app.inject({ method: "POST", url: "/api/chat", payload: { message: "salut" } });
expect(res.statusCode).toBe(401);
});
it("/api/chat répond avec token", async () => {
const token = await tokenOf();
const res = await app.inject({
method: "POST",
url: "/api/chat",
headers: { authorization: `Bearer ${token}` },
payload: { message: "salut" },
});
expect(res.statusCode).toBe(200);
expect(res.json().reply).toBe("echo:salut");
});
});
describe("API review", () => {
it("liste, approuve et refuse", async () => {
repo.create(createAsset({ id: "tool:x", type: "tool", version: "1.0.0", riskTier: "privileged" }));
const token = await tokenOf();
const auth_h = { authorization: `Bearer ${token}` };
const list = await app.inject({ method: "GET", url: "/api/review", headers: auth_h });
expect(list.json().assets).toHaveLength(1);
const ok = await app.inject({ method: "POST", url: "/api/review/tool:x/approve", headers: auth_h });
expect(ok.json().asset.status).toBe("approuvé");
const missing = await app.inject({ method: "POST", url: "/api/review/nope/refuse", headers: auth_h });
expect(missing.statusCode).toBe(404);
});
});