/** * contextBridge.test.ts — Unit test S29 * * Testa contextBridge: * - enrichSystemPrompt: output compatto + sezioni corrette + max chars * - enrichSystemPrompt: vuoto se brain è empty * - enrichSystemPrompt: rilevanza pagine filtrata per userMsg * - enrichSystemPrompt: non crasha mai (try/catch) * - recordToolResult: read_page → pageCache.set * - recordToolResult: fetch_url → pageCache.set * - recordToolResult: write_file → docIndex.add con lang corretto * - recordToolResult: ❌ errore → errorLog.record * - recordToolResult: tool sconosciuto non crasha * - recordToolResult: url mancante → no call a pageCache * - recordToolResult: path mancante → no call a docIndex * - _setTestBrain: sostituisce brain reale * * Mock: _setTestBrain() (DI) — zero vi.mock su moduli globali. */ import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest"; import { enrichSystemPrompt, recordToolResult, _setTestBrain, } from "@/lib/contextBridge"; import type { agentBrain } from "@/lib/agentBrain"; // ─── Mock brain factory ──────────────────────────────────────────────────────── type BrainFacade = typeof agentBrain; function makeMockBrain(overrides: { resolvedWithFix?: (_lang?: string) => Array<{ message: string; fix?: string }>; recent?: (_n?: number) => ReturnType; docSearch?: (_q: string) => ReturnType; pageSet?: ReturnType; errorRecord?: ReturnType; docAdd?: ReturnType; } = {}): BrainFacade { const pageSet = overrides.pageSet ?? vi.fn(); const errorRecord = overrides.errorRecord ?? vi.fn(); const docAdd = overrides.docAdd ?? vi.fn(); return { pageCache: { set: pageSet, recent: overrides.recent ?? ((_n?: number) => []), search: (_q: string) => [], get: (_url: string) => null, has: (_url: string) => false, byDomain: (_d: string) => [], size: () => 0, clear: () => {}, }, errorLog: { resolvedWithFix: overrides.resolvedWithFix ?? ((_lang?: string) => []), record: errorRecord, markFixed: (_h: string, _f: string) => false, findSimilar: (_m: string) => [], unresolved: (_l?: string) => [], recent: (_n?: number) => [], hashOf: (_m: string) => "", size: () => 0, clear: () => {}, }, docIndex: { search: overrides.docSearch ?? ((_q: string) => []), add: docAdd, get: (_p: string) => null, has: (_p: string) => false, isStale: (_p: string, _c: string) => true, byTag: (_t: string) => [], byLang: (_l: string) => [], recent: (_n?: number) => [], size: () => 0, clear: () => {}, }, initBrain: async () => {}, } as unknown as BrainFacade; } // ─── Setup ──────────────────────────────────────────────────────────────────── const emptyBrain = makeMockBrain(); beforeAll(() => { _setTestBrain(emptyBrain); }); beforeEach(() => { vi.clearAllMocks(); _setTestBrain(emptyBrain); }); // ─── enrichSystemPrompt — brain vuoto ──────────────────────────────────────── describe("enrichSystemPrompt — brain vuoto", () => { it("ritorna stringa vuota se nessun dato", () => { expect(enrichSystemPrompt("test query")).toBe(""); }); it("non crasha mai (try/catch interno)", () => { const brokenBrain = makeMockBrain({ resolvedWithFix: () => { throw new Error("broken"); }, }); _setTestBrain(brokenBrain); expect(() => enrichSystemPrompt("query")).not.toThrow(); expect(enrichSystemPrompt("query")).toBe(""); }); it("stringa query vuota non crasha", () => { expect(() => enrichSystemPrompt("")).not.toThrow(); expect(enrichSystemPrompt("")).toBe(""); }); }); // ─── enrichSystemPrompt — fix noti ─────────────────────────────────────────── describe("enrichSystemPrompt — fix noti", () => { it("include sezione [FIX NOTI] con errori risolti", () => { const brain = makeMockBrain({ resolvedWithFix: () => [ { message: "TS2353: Object literal extra property", fix: "rimuovi il campo extra" }, ], }); _setTestBrain(brain); const result = enrichSystemPrompt("debug TypeScript"); expect(result).toContain("[FIX NOTI]"); expect(result).toContain("TS2353"); expect(result).toContain("rimuovi il campo extra"); }); it("max 3 fix nel contesto (MAX_ERRORS_IN_CTX)", () => { const manyFixes = Array.from({ length: 10 }, (_, i) => ({ message: `Error${i}`, fix: `Fix${i}`, })); const brain = makeMockBrain({ resolvedWithFix: () => manyFixes }); _setTestBrain(brain); const result = enrichSystemPrompt("query"); const fixLines = result.split("\n").filter(l => l.startsWith("•")); expect(fixLines.length).toBeLessThanOrEqual(3); }); it("non include [FIX NOTI] se nessun fix disponibile", () => { const brain = makeMockBrain({ resolvedWithFix: () => [] }); _setTestBrain(brain); expect(enrichSystemPrompt("query")).not.toContain("[FIX NOTI]"); }); it("tronca messaggi lunghi a 70 chars", () => { const longMsg = "A".repeat(200); const brain = makeMockBrain({ resolvedWithFix: () => [{ message: longMsg, fix: "B".repeat(200) }], }); _setTestBrain(brain); const result = enrichSystemPrompt("query"); const fixLine = result.split("\n").find(l => l.startsWith("•")); expect(fixLine).toBeDefined(); // message + fix are each ≤70, so line ≤ ~150 chars (plus " → ") expect((fixLine ?? "").length).toBeLessThanOrEqual(160); }); }); // ─── enrichSystemPrompt — pagine lette ─────────────────────────────────────── describe("enrichSystemPrompt — pagine lette", () => { it("include [PAGINE GIÀ LETTE] con le pagine recenti", () => { const brain = makeMockBrain({ recent: () => [{ url: "https://react.dev/docs/hooks", title: "React Hooks", summary: "Hooks documentation", domain: "react.dev", visitedAt: Date.now(), visits: 1, size: 100, }], }); _setTestBrain(brain); const result = enrichSystemPrompt("react hooks tutorial"); expect(result).toContain("[PAGINE GIÀ LETTE]"); expect(result).toContain("react.dev"); expect(result).toContain("React Hooks"); }); it("filtra per rilevanza: python.org più rilevante per query python", () => { const brain = makeMockBrain({ recent: () => [ { url: "https://react.dev", title: "React docs", summary: "react documentation hooks", domain: "react.dev", visitedAt: Date.now() - 1000, visits: 1, size: 100 }, { url: "https://python.org", title: "Python docs", summary: "python programming language", domain: "python.org", visitedAt: Date.now(), visits: 1, size: 100 }, ], }); _setTestBrain(brain); const result = enrichSystemPrompt("python programming tutorial"); expect(result).toContain("python.org"); }); it("S97: nessun fallback cieco — pagine irrilevanti non appaiono nel prompt", () => { const brain = makeMockBrain({ recent: () => [ { url: "https://example.com/a", title: "Page A", summary: "content A", domain: "example.com", visitedAt: Date.now() - 2000, visits: 1, size: 100 }, { url: "https://example.com/b", title: "Page B", summary: "content B", domain: "example.com", visitedAt: Date.now() - 1000, visits: 1, size: 100 }, ], }); _setTestBrain(brain); const result = enrichSystemPrompt("completely unrelated zzz query"); // S97: rimosso fallback cieco (threshold 0.1 → 0.3) — pagine irrilevanti non vengono iniettate expect(result).not.toContain("[PAGINE GIÀ LETTE]"); }); it("no [PAGINE GIÀ LETTE] se pageCache è vuoto", () => { const brain = makeMockBrain({ recent: () => [] }); _setTestBrain(brain); expect(enrichSystemPrompt("react")).not.toContain("[PAGINE GIÀ LETTE]"); }); }); // ─── enrichSystemPrompt — docs indicizzati ─────────────────────────────────── describe("enrichSystemPrompt — docs indicizzati", () => { it("include [DOCS INDICIZZATI] con documenti rilevanti", () => { const brain = makeMockBrain({ docSearch: () => [{ path: "src/lib/agentBrain", title: "agentBrain.ts", summary: "browser memory system", tags: ["memory", "dexie"], contentHash: "abc", indexedAt: Date.now(), lang: "typescript", size: 500, }], }); _setTestBrain(brain); const result = enrichSystemPrompt("memory dexie"); expect(result).toContain("[DOCS INDICIZZATI]"); expect(result).toContain("agentBrain"); }); it("aggiunge estensione .ts per typescript", () => { const brain = makeMockBrain({ docSearch: () => [{ path: "src/lib/utils", title: "utils", summary: "utility functions", tags: [], contentHash: "x", indexedAt: Date.now(), lang: "typescript", size: 100, }], }); _setTestBrain(brain); expect(enrichSystemPrompt("utils")).toContain(".ts"); }); it("aggiunge estensione .py per python", () => { const brain = makeMockBrain({ docSearch: () => [{ path: "scripts/runner", title: "runner", summary: "script", tags: [], contentHash: "y", indexedAt: Date.now(), lang: "python", size: 100, }], }); _setTestBrain(brain); expect(enrichSystemPrompt("script")).toContain(".py"); }); it("no [DOCS INDICIZZATI] se docIndex.search ritorna []", () => { const brain = makeMockBrain({ docSearch: () => [] }); _setTestBrain(brain); expect(enrichSystemPrompt("query")).not.toContain("[DOCS INDICIZZATI]"); }); }); // ─── enrichSystemPrompt — max chars ────────────────────────────────────────── describe("enrichSystemPrompt — max chars", () => { it("output ≤ 801 chars (800 + ellipsis)", () => { const longFix = { message: "E".repeat(200), fix: "F".repeat(200) }; const brain = makeMockBrain({ resolvedWithFix: () => [longFix, longFix, longFix], recent: () => [{ url: "https://example.com/" + "x".repeat(100), title: "T".repeat(100), summary: "S".repeat(500), domain: "example.com", visitedAt: Date.now(), visits: 1, size: 1000, }], }); _setTestBrain(brain); const result = enrichSystemPrompt("query"); expect(result.length).toBeLessThanOrEqual(801); }); it("aggiunge '…' se tronca", () => { const longFix = { message: "E".repeat(300), fix: "F".repeat(300) }; const brain = makeMockBrain({ resolvedWithFix: () => [longFix, longFix, longFix] }); _setTestBrain(brain); const result = enrichSystemPrompt("query"); if (result.length > 800) { expect(result.endsWith("…")).toBe(true); } }); it("output breve se solo 1 fix corto — no troncamento", () => { const brain = makeMockBrain({ resolvedWithFix: () => [{ message: "Short error", fix: "quick fix" }], }); _setTestBrain(brain); const result = enrichSystemPrompt("query"); expect(result.length).toBeLessThan(200); expect(result.endsWith("…")).toBe(false); }); }); // ─── recordToolResult — read_page ──────────────────────────────────────────── describe("recordToolResult — read_page", () => { it("chiama pageCache.set con url, title estratto, summary, size", () => { const pageSet = vi.fn(); _setTestBrain(makeMockBrain({ pageSet })); recordToolResult("read_page", { url: "https://example.com" }, "# Hello World\nContent here"); expect(pageSet).toHaveBeenCalledOnce(); const [url, data] = pageSet.mock.calls[0]!; expect(url).toBe("https://example.com"); expect(data.title).toBe("Hello World"); expect(data.summary).toContain("Content"); expect(data.size).toBeGreaterThan(0); }); it("usa shortDomain come title se prima riga è vuota", () => { const pageSet = vi.fn(); _setTestBrain(makeMockBrain({ pageSet })); recordToolResult("read_page", { url: "https://github.com/user/repo" }, "\n\nno heading"); const [, data] = pageSet.mock.calls[0]!; expect(data.title).toContain("github.com"); }); it("non chiama pageCache.set se url è assente", () => { const pageSet = vi.fn(); _setTestBrain(makeMockBrain({ pageSet })); recordToolResult("read_page", {}, "some content"); expect(pageSet).not.toHaveBeenCalled(); }); it("tronca summary a 500 chars", () => { const pageSet = vi.fn(); _setTestBrain(makeMockBrain({ pageSet })); recordToolResult("read_page", { url: "https://x.com" }, "T\n" + "C".repeat(1000)); const [, data] = pageSet.mock.calls[0]!; expect(data.summary.length).toBeLessThanOrEqual(500); }); }); // ─── recordToolResult — fetch_url ──────────────────────────────────────────── describe("recordToolResult — fetch_url", () => { it("chiama pageCache.set come read_page", () => { const pageSet = vi.fn(); _setTestBrain(makeMockBrain({ pageSet })); recordToolResult("fetch_url", { url: "https://api.example.com/data" }, "json data"); expect(pageSet).toHaveBeenCalledOnce(); expect(pageSet.mock.calls[0]![0]).toBe("https://api.example.com/data"); }); }); // ─── recordToolResult — write_file ─────────────────────────────────────────── describe("recordToolResult — write_file", () => { it("chiama docIndex.add con lang typescript per .ts", () => { const docAdd = vi.fn(); _setTestBrain(makeMockBrain({ docAdd })); recordToolResult( "write_file", { path: "src/lib/myModule.ts", content: "export const x = 1;" }, "✅ Scritto" ); expect(docAdd).toHaveBeenCalledOnce(); const [path, data] = docAdd.mock.calls[0]!; expect(path).toBe("src/lib/myModule.ts"); expect(data.lang).toBe("typescript"); expect(data.title).toBe("myModule.ts"); }); it("lang python per .py", () => { const docAdd = vi.fn(); _setTestBrain(makeMockBrain({ docAdd })); recordToolResult("write_file", { path: "script.py", content: "print('hi')" }, "✅"); expect(docAdd.mock.calls[0]![1].lang).toBe("python"); }); it("lang javascript per .js", () => { const docAdd = vi.fn(); _setTestBrain(makeMockBrain({ docAdd })); recordToolResult("write_file", { path: "app.js", content: "const x = 1" }, "✅"); expect(docAdd.mock.calls[0]![1].lang).toBe("javascript"); }); it("lang typescript per .tsx", () => { const docAdd = vi.fn(); _setTestBrain(makeMockBrain({ docAdd })); recordToolResult("write_file", { path: "App.tsx", content: "" }, "✅"); expect(docAdd.mock.calls[0]![1].lang).toBe("typescript"); }); it("non chiama docIndex.add se path mancante", () => { const docAdd = vi.fn(); _setTestBrain(makeMockBrain({ docAdd })); recordToolResult("write_file", {}, "content"); expect(docAdd).not.toHaveBeenCalled(); }); it("usa nome file come title", () => { const docAdd = vi.fn(); _setTestBrain(makeMockBrain({ docAdd })); recordToolResult("write_file", { path: "deep/nested/file.ts", content: "x" }, "✅"); expect(docAdd.mock.calls[0]![1].title).toBe("file.ts"); }); }); // ─── recordToolResult — errori ──────────────────────────────────────────────── describe("recordToolResult — errori", () => { it("chiama errorLog.record per result che inizia con ❌", () => { const errorRecord = vi.fn(); _setTestBrain(makeMockBrain({ errorRecord })); recordToolResult("web_search", { query: "test" }, "❌ Fetch failed: timeout"); expect(errorRecord).toHaveBeenCalledOnce(); const [msg, ctx] = errorRecord.mock.calls[0]!; expect(msg).toContain("❌"); expect(ctx).toBe("tool:web_search"); }); it("run_code errore → lang=javascript", () => { const errorRecord = vi.fn(); _setTestBrain(makeMockBrain({ errorRecord })); recordToolResult("run_code", { code: "x()" }, "❌ ReferenceError: x is not defined"); expect(errorRecord.mock.calls[0]![2]).toBe("javascript"); }); it("execute_shell errore → lang=javascript", () => { const errorRecord = vi.fn(); _setTestBrain(makeMockBrain({ errorRecord })); recordToolResult("execute_shell", { command: "node x.js" }, "❌ Error: ENOENT"); expect(errorRecord.mock.calls[0]![2]).toBe("javascript"); }); it("web_search errore → lang=undefined", () => { const errorRecord = vi.fn(); _setTestBrain(makeMockBrain({ errorRecord })); recordToolResult("web_search", {}, "❌ Network error"); expect(errorRecord.mock.calls[0]![2]).toBeUndefined(); }); it("tool sconosciuto con successo → non crasha, non chiama nulla", () => { const pageSet = vi.fn(); const docAdd = vi.fn(); const errorRecord = vi.fn(); _setTestBrain(makeMockBrain({ pageSet, docAdd, errorRecord })); expect(() => recordToolResult("unknown_tool", {}, "some output")).not.toThrow(); expect(pageSet).not.toHaveBeenCalled(); expect(docAdd).not.toHaveBeenCalled(); expect(errorRecord).not.toHaveBeenCalled(); }); it("fire-and-forget: non lancia anche se brain è broken", () => { const brokenBrain = makeMockBrain({ pageSet: vi.fn(() => { throw new Error("broken"); }), errorRecord: vi.fn(() => { throw new Error("broken"); }), docAdd: vi.fn(() => { throw new Error("broken"); }), }); _setTestBrain(brokenBrain); expect(() => recordToolResult("read_page", { url: "https://x.com" }, "content")).not.toThrow(); expect(() => recordToolResult("write_file", { path: "x.ts", content: "x" }, "✅")).not.toThrow(); expect(() => recordToolResult("web_search", {}, "❌ error")).not.toThrow(); }); });