AUDIT / src /lib /__tests__ /errorRecovery.test.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
13.5 kB
/**
* 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();
});
});