Spaces:
Sleeping
Sleeping
| /** | |
| * 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<BrainFacade["pageCache"]["recent"]>; | |
| docSearch?: (_q: string) => ReturnType<BrainFacade["docIndex"]["search"]>; | |
| pageSet?: ReturnType<typeof vi.fn>; | |
| errorRecord?: ReturnType<typeof vi.fn>; | |
| docAdd?: ReturnType<typeof vi.fn>; | |
| } = {}): 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: "<App/>" }, "✅"); | |
| 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(); | |
| }); | |
| }); | |