Spaces:
Sleeping
Sleeping
| /** | |
| * 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> = {}): 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<typeof vi.fn>; | |
| markFixed: ReturnType<typeof vi.fn>; | |
| findSimilar: ReturnType<typeof vi.fn>; | |
| hashOf: ReturnType<typeof vi.fn>; | |
| }; | |
| function makeMockLog(overrides: Partial<MockLog> = {}): 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(); | |
| }); | |
| }); | |