/** * errorAutoFix.test.ts — Unit test S32 * * Copre: * - startErrorAutoFix: avvia il timer (isAutoFixRunning = true) * - startErrorAutoFix: idempotente (chiamate multiple non duplicano) * - stopErrorAutoFix: ferma il timer (isAutoFixRunning = false) * - _tick: no backend URL → skip silenzioso * - _tick: errori < THRESHOLD → nessun debugLoop.run * - _tick: errori ≥ THRESHOLD, lang=typescript → debugLoop.run chiamato * - _tick: debugLoop.run success → errorLog.markFixed chiamato * - _tick: debugLoop.run failure → markFixed non chiamato * - _tick: cooldown attivo → debugLoop.run non chiamato una seconda volta * - _tick: già in running → skip (no doppia esecuzione) * - _tick: debugLoop lancia → non crasha (non-fatal) * - clearCooldowns: svuota cooldown map */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { startErrorAutoFix, stopErrorAutoFix, isAutoFixRunning, clearCooldowns, _setTestDeps, _resetDeps, _tickForTest, } from "@/lib/errorAutoFix"; import type { ErrorEntry } from "@/lib/agentBrain"; // ─── Helper ─────────────────────────────────────────────────────────────────── function makeEntry(overrides: Partial = {}): ErrorEntry { return { hash: "h1", message: "SyntaxError: Unexpected token", context: "tool:run_code", fix: undefined, count: 3, firstSeen: 1000, lastSeen: Date.now(), lang: "typescript", resolved: false, ...overrides, }; } function makeMockDebugLoop(success = true, times = 1) { return { run: vi.fn().mockResolvedValue({ success, finalCode: success ? "const x = 1;" : "", attempts: times, }), }; } // ─── Setup / Teardown ───────────────────────────────────────────────────────── beforeEach(() => { stopErrorAutoFix(); clearCooldowns(); _resetDeps(); // Resetta ENV mock vi.unstubAllEnvs(); }); afterEach(() => { stopErrorAutoFix(); clearCooldowns(); _resetDeps(); }); // ─── startErrorAutoFix / stopErrorAutoFix ───────────────────────────────────── describe("startErrorAutoFix / stopErrorAutoFix", () => { it("startErrorAutoFix avvia il loop (isAutoFixRunning = true)", () => { startErrorAutoFix(60_000); expect(isAutoFixRunning()).toBe(true); stopErrorAutoFix(); }); it("stopErrorAutoFix ferma il loop (isAutoFixRunning = false)", () => { startErrorAutoFix(60_000); stopErrorAutoFix(); expect(isAutoFixRunning()).toBe(false); }); it("startErrorAutoFix è idempotente: doppia chiamata non duplica", () => { startErrorAutoFix(60_000); startErrorAutoFix(60_000); // seconda chiamata: ignorata expect(isAutoFixRunning()).toBe(true); stopErrorAutoFix(); expect(isAutoFixRunning()).toBe(false); }); }); // ─── _tick: condizioni di skip ──────────────────────────────────────────────── describe("_tick: condizioni di skip", () => { it("nessun BACKEND_URL → skip silenzioso, debugLoop non chiamato", async () => { const mockLog = { unresolved: vi.fn().mockReturnValue([makeEntry(), makeEntry(), makeEntry()]), markFixed: vi.fn() }; const mockLoop = makeMockDebugLoop(); _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); // ENV.BACKEND_URL è vuoto per default in test await _tickForTest(); expect(mockLoop.run).not.toHaveBeenCalled(); }); it("errori < THRESHOLD (3) → nessun debugLoop.run", async () => { const mockLog = { unresolved: vi.fn().mockReturnValue([makeEntry(), makeEntry()]), // solo 2 markFixed: vi.fn(), }; const mockLoop = makeMockDebugLoop(); _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); await _tickForTest(); expect(mockLoop.run).not.toHaveBeenCalled(); }); it("nessun errore → nessun debugLoop.run", async () => { const mockLog = { unresolved: vi.fn().mockReturnValue([]), markFixed: vi.fn() }; const mockLoop = makeMockDebugLoop(); _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); await _tickForTest(); expect(mockLoop.run).not.toHaveBeenCalled(); }); }); // ─── _tick: auto-fix attivo ─────────────────────────────────────────────────── describe("_tick: auto-fix con backend", () => { beforeEach(() => { // Stub BACKEND_URL tramite ENV mock nel modulo // Non possiamo stubare import.meta.env direttamente, usiamo _setTestDeps // con debugLoop già iniettato (così il modulo non tenta import lazy) }); it("≥3 errori typescript + debugLoop iniettato + success → markFixed chiamato", async () => { const entries = [makeEntry({ hash: "h1" }), makeEntry({ hash: "h1" }), makeEntry({ hash: "h1" })]; const mockLog = { unresolved: vi.fn().mockReturnValue(entries), markFixed: vi.fn().mockReturnValue(true) }; const mockLoop = makeMockDebugLoop(true, 1); // Inietta debugLoop direttamente E simula BACKEND_URL settato _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); // Modifichiamo ENV tramite patch sull'oggetto importato const envModule = await import("@/config/env"); const origUrl = envModule.ENV.BACKEND_URL; (envModule.ENV as Record).BACKEND_URL = "https://backend.example.com"; await _tickForTest(); (envModule.ENV as Record).BACKEND_URL = origUrl; expect(mockLoop.run).toHaveBeenCalledTimes(1); expect(mockLog.markFixed).toHaveBeenCalledTimes(1); const [hash, fixNote] = mockLoop.run.mock.calls.length > 0 ? [mockLog.markFixed.mock.calls[0][0], mockLog.markFixed.mock.calls[0][1]] : ["", ""]; expect(hash).toBe("h1"); expect(fixNote).toContain("Auto-fix"); }); it("debugLoop failure → markFixed non chiamato", async () => { const entries = [makeEntry({ hash: "h2" }), makeEntry({ hash: "h2" }), makeEntry({ hash: "h2" })]; const mockLog = { unresolved: vi.fn().mockReturnValue(entries), markFixed: vi.fn() }; const mockLoop = makeMockDebugLoop(false); _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); const envModule = await import("@/config/env"); const origUrl = envModule.ENV.BACKEND_URL; (envModule.ENV as Record).BACKEND_URL = "https://backend.example.com"; await _tickForTest(); (envModule.ENV as Record).BACKEND_URL = origUrl; expect(mockLoop.run).toHaveBeenCalledTimes(1); expect(mockLog.markFixed).not.toHaveBeenCalled(); }); it("cooldown attivo → secondo tick non chiama debugLoop", async () => { clearCooldowns(); const entries = [makeEntry({ hash: "h3" }), makeEntry({ hash: "h3" }), makeEntry({ hash: "h3" })]; const mockLog = { unresolved: vi.fn().mockReturnValue(entries), markFixed: vi.fn() }; const mockLoop = makeMockDebugLoop(true); _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); const envModule = await import("@/config/env"); const origUrl = envModule.ENV.BACKEND_URL; (envModule.ENV as Record).BACKEND_URL = "https://backend.example.com"; await _tickForTest(); // prima esecuzione → cooldown impostato await _tickForTest(); // seconda esecuzione → h3 in cooldown (envModule.ENV as Record).BACKEND_URL = origUrl; // debugLoop chiamato solo una volta expect(mockLoop.run).toHaveBeenCalledTimes(1); }); it("debugLoop lancia → non crasha (non-fatal)", async () => { const entries = [makeEntry({ hash: "h4" }), makeEntry({ hash: "h4" }), makeEntry({ hash: "h4" })]; const mockLog = { unresolved: vi.fn().mockReturnValue(entries), markFixed: vi.fn() }; const mockLoop = { run: vi.fn().mockRejectedValue(new Error("Network timeout")) }; _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); const envModule = await import("@/config/env"); const origUrl = envModule.ENV.BACKEND_URL; (envModule.ENV as Record).BACKEND_URL = "https://backend.example.com"; await expect(_tickForTest()).resolves.not.toThrow(); (envModule.ENV as Record).BACKEND_URL = origUrl; }); }); // ─── clearCooldowns ─────────────────────────────────────────────────────────── describe("clearCooldowns", () => { it("clearCooldowns permette di ritentare dopo cooldown", async () => { const entries = [makeEntry({ hash: "h5" }), makeEntry({ hash: "h5" }), makeEntry({ hash: "h5" })]; const mockLog = { unresolved: vi.fn().mockReturnValue(entries), markFixed: vi.fn() }; const mockLoop = makeMockDebugLoop(true); _setTestDeps({ errorLog: mockLog, debugLoop: mockLoop }); const envModule = await import("@/config/env"); const origUrl = envModule.ENV.BACKEND_URL; (envModule.ENV as Record).BACKEND_URL = "https://backend.example.com"; await _tickForTest(); // imposta cooldown clearCooldowns(); // resetta await _tickForTest(); // ora può ritentare (envModule.ENV as Record).BACKEND_URL = origUrl; expect(mockLoop.run).toHaveBeenCalledTimes(2); }); });