/** * errorRecovery.test.ts — Unit test S31 * * Testa errorRecovery: * - recordAndSuggestFix: nessun fix noto → {hadFix:false, count:1} * - recordAndSuggestFix: fix noto trovato → {hadFix:true, fix, count} * - recordAndSuggestFix: errore con prefisso ❌ rimosso correttamente * - recordAndSuggestFix: fallback strategy per tool noti (web_search, etc.) * - recordAndSuggestFix: non crasha mai (DI mock che lancia) * - recordAndSuggestFix: conta le occorrenze (count incrementa) * - notifyToolSuccess: nessun pending → ritorna false * - notifyToolSuccess: pending trovato → markFixed chiamato → ritorna true * - notifyToolSuccess: rimuove il pending dopo markFixed * - notifyToolSuccess: non crasha se markFixed lancia * - buildRecoveryMessage: stringa vuota se failures=[] * - buildRecoveryMessage: include fix noto con emoji 💡 * - buildRecoveryMessage: include hint con emoji ⚡ se nessun fix * - buildRecoveryMessage: fallback generico senza fix né hint * - getPendingFix: undefined se nessun fix suggerito * - getPendingFix: ritorna fix dopo recordAndSuggestFix con fix noto * - clearPendingFixes: svuota tutti i pending * - _setTestLog: sostituisce errorLog reale con mock * * Mock: _setTestLog() (DI) — zero vi.mock su moduli globali. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { recordAndSuggestFix, notifyToolSuccess, buildRecoveryMessage, getPendingFix, clearPendingFixes, _setTestLog, _resetLog, type RecoveryContext, } from "@/lib/agent/errorRecovery"; import type { ErrorEntry } from "@/lib/agentBrain"; // ─── Mock ErrorLog factory ──────────────────────────────────────────────────── function makeErrorEntry(overrides: Partial = {}): ErrorEntry { return { hash: "abc123", message: "test error", context: "tool:web_search", fix: undefined, count: 1, firstSeen: 1000, lastSeen: 2000, lang: undefined, resolved: false, ...overrides, }; } type MockLog = { record: ReturnType; markFixed: ReturnType; findSimilar: ReturnType; hashOf: ReturnType; }; function makeMockLog(overrides: Partial = {}): MockLog { return { record: overrides.record ?? vi.fn().mockReturnValue(makeErrorEntry()), markFixed: overrides.markFixed ?? vi.fn().mockReturnValue(true), findSimilar: overrides.findSimilar ?? vi.fn().mockReturnValue([]), hashOf: overrides.hashOf ?? vi.fn().mockReturnValue("abc123"), }; } // ─── Setup / Teardown ───────────────────────────────────────────────────────── let mockLog: MockLog; beforeEach(() => { mockLog = makeMockLog(); _setTestLog(mockLog); clearPendingFixes(); }); afterEach(() => { _resetLog(); clearPendingFixes(); }); // ─── recordAndSuggestFix ────────────────────────────────────────────────────── describe("recordAndSuggestFix", () => { it("nessun fix noto → hadFix false, count=1", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry({ count: 1 })); const ctx: RecoveryContext = { toolName: "web_search", errorMsg: "❌ Network error" }; const r = recordAndSuggestFix(ctx); expect(r.hadFix).toBe(false); expect(r.fix).toBeUndefined(); expect(r.count).toBe(1); expect(mockLog.record).toHaveBeenCalledWith("Network error", "tool:web_search", undefined, undefined); }); it("fix noto trovato → hadFix true, fix presente", () => { const known = makeErrorEntry({ resolved: true, fix: "Usa fetch_url invece di web_search per URL diretti.", }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ count: 3, hash: known.hash })); const ctx: RecoveryContext = { toolName: "web_search", errorMsg: "❌ Network error" }; const r = recordAndSuggestFix(ctx); expect(r.hadFix).toBe(true); expect(r.fix).toBe("Usa fetch_url invece di web_search per URL diretti."); expect(r.count).toBe(3); }); it("prefisso ❌ rimosso prima di record e findSimilar", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry()); recordAndSuggestFix({ toolName: "read_file", errorMsg: "❌ File non trovato" }); expect(mockLog.findSimilar).toHaveBeenCalledWith("File non trovato"); expect(mockLog.record).toHaveBeenCalledWith("File non trovato", "tool:read_file", undefined, undefined); }); it("errore senza prefisso ❌ funziona ugualmente", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry()); const r = recordAndSuggestFix({ toolName: "fetch_url", errorMsg: "Timeout error" }); expect(r.hadFix).toBe(false); expect(mockLog.record).toHaveBeenCalledWith("Timeout error", "tool:fetch_url", undefined, undefined); }); it("fallback strategy per web_search quando nessun fix", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry()); const r = recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ error" }); expect(r.hint).toContain("fetch_url"); }); it("fallback strategy per write_file quando nessun fix", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry()); const r = recordAndSuggestFix({ toolName: "write_file", errorMsg: "❌ Permission denied" }); expect(r.hint).toContain("percorso"); }); it("tool senza fallback strategy → hint undefined", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry()); const r = recordAndSuggestFix({ toolName: "custom_tool_xyz", errorMsg: "❌ error" }); expect(r.hint).toBeUndefined(); }); it("fix risolto ma resolved=false → ignorato (non usato come fix)", () => { const unresolved = makeErrorEntry({ fix: "fix vecchio", resolved: false }); mockLog.findSimilar.mockReturnValue([unresolved]); mockLog.record.mockReturnValue(makeErrorEntry()); const r = recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ error" }); expect(r.hadFix).toBe(false); }); it("count incrementa su chiamate multiple (dal mock)", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry({ count: 5 })); const r = recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ error" }); expect(r.count).toBe(5); }); it("non crasha se findSimilar lancia", () => { mockLog.findSimilar.mockImplementation(() => { throw new Error("DB error"); }); mockLog.record.mockReturnValue(makeErrorEntry()); expect(() => recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ x" })).not.toThrow(); }); it("non crasha se record lancia", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockImplementation(() => { throw new Error("DB write error"); }); expect(() => recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ x" })).not.toThrow(); }); it("lang passato correttamente a errorLog.record", () => { mockLog.findSimilar.mockReturnValue([]); mockLog.record.mockReturnValue(makeErrorEntry()); recordAndSuggestFix({ toolName: "run_code", errorMsg: "❌ SyntaxError", lang: "typescript" }); expect(mockLog.record).toHaveBeenCalledWith("SyntaxError", "tool:run_code", "typescript", undefined); }); }); // ─── notifyToolSuccess ──────────────────────────────────────────────────────── describe("notifyToolSuccess", () => { it("nessun pending fix → ritorna false", () => { expect(notifyToolSuccess("web_search")).toBe(false); expect(mockLog.markFixed).not.toHaveBeenCalled(); }); it("pending trovato → markFixed chiamato → ritorna true", () => { const known = makeErrorEntry({ resolved: true, fix: "Usa fetch_url.", hash: "h1" }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ hash: "h1", count: 1 })); mockLog.markFixed.mockReturnValue(true); recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ error" }); const ok = notifyToolSuccess("web_search"); expect(ok).toBe(true); expect(mockLog.markFixed).toHaveBeenCalledWith("h1", "Usa fetch_url."); }); it("pending rimosso dopo notifyToolSuccess", () => { const known = makeErrorEntry({ resolved: true, fix: "fix A", hash: "hA" }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ hash: "hA" })); recordAndSuggestFix({ toolName: "fetch_url", errorMsg: "❌ error" }); notifyToolSuccess("fetch_url"); // Second call — no more pending expect(notifyToolSuccess("fetch_url")).toBe(false); }); it("non crasha se markFixed lancia", () => { const known = makeErrorEntry({ resolved: true, fix: "fix", hash: "hB" }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ hash: "hB" })); mockLog.markFixed.mockImplementation(() => { throw new Error("DB error"); }); recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ err" }); expect(() => notifyToolSuccess("web_search")).not.toThrow(); }); it("appliedFix override usato invece del fix suggerito", () => { const known = makeErrorEntry({ resolved: true, fix: "fix originale", hash: "hC" }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ hash: "hC" })); mockLog.markFixed.mockReturnValue(true); recordAndSuggestFix({ toolName: "write_file", errorMsg: "❌ err" }); notifyToolSuccess("write_file", "fix custom applicato"); expect(mockLog.markFixed).toHaveBeenCalledWith("hC", "fix custom applicato"); }); }); // ─── buildRecoveryMessage ───────────────────────────────────────────────────── describe("buildRecoveryMessage", () => { it("stringa vuota se failures=[]", () => { expect(buildRecoveryMessage([])).toBe(""); }); it("include fix noto con emoji 💡", () => { const msg = buildRecoveryMessage([ { toolName: "web_search", result: { hash: "x", hadFix: true, fix: "Usa fetch_url", count: 2 } }, ]); expect(msg).toContain("💡"); expect(msg).toContain("Usa fetch_url"); expect(msg).toContain("web_search"); }); it("include hint con emoji ⚡ se nessun fix noto", () => { const msg = buildRecoveryMessage([ { toolName: "fetch_url", result: { hash: "x", hadFix: false, hint: "Prova URL alternativo", count: 1 } }, ]); expect(msg).toContain("⚡"); expect(msg).toContain("Prova URL alternativo"); }); it("fallback generico se né fix né hint", () => { const msg = buildRecoveryMessage([ { toolName: "custom_tool", result: { hash: "x", hadFix: false, count: 3 } }, ]); expect(msg).toContain("custom_tool"); expect(msg).toContain("3x"); }); it("include 'Continua il task' in ogni caso", () => { const msg = buildRecoveryMessage([ { toolName: "web_search", result: { hash: "x", hadFix: true, fix: "fix", count: 1 } }, ]); expect(msg).toContain("Continua il task"); }); it("gestisce più failure contemporaneamente", () => { const msg = buildRecoveryMessage([ { toolName: "web_search", result: { hash: "x", hadFix: true, fix: "fix A", count: 1 } }, { toolName: "fetch_url", result: { hash: "y", hadFix: false, hint: "hint B", count: 2 } }, ]); expect(msg).toContain("web_search"); expect(msg).toContain("fetch_url"); }); }); // ─── getPendingFix / clearPendingFixes ──────────────────────────────────────── describe("getPendingFix / clearPendingFixes", () => { it("undefined se nessun fix suggerito", () => { expect(getPendingFix("web_search")).toBeUndefined(); }); it("ritorna fix dopo recordAndSuggestFix con fix noto", () => { const known = makeErrorEntry({ resolved: true, fix: "fix trovato", hash: "hD" }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ hash: "hD" })); recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ error" }); const pending = getPendingFix("web_search"); expect(pending).toBeDefined(); expect(pending?.fix).toBe("fix trovato"); expect(pending?.hash).toBe("hD"); }); it("clearPendingFixes svuota tutto", () => { const known = makeErrorEntry({ resolved: true, fix: "fix", hash: "hE" }); mockLog.findSimilar.mockReturnValue([known]); mockLog.record.mockReturnValue(makeErrorEntry({ hash: "hE" })); recordAndSuggestFix({ toolName: "web_search", errorMsg: "❌ err" }); clearPendingFixes(); expect(getPendingFix("web_search")).toBeUndefined(); }); });