Spaces:
Sleeping
Sleeping
| /** | |
| * 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> = {}): 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<string, unknown>).BACKEND_URL = "https://backend.example.com"; | |
| await _tickForTest(); | |
| (envModule.ENV as Record<string, unknown>).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<string, unknown>).BACKEND_URL = "https://backend.example.com"; | |
| await _tickForTest(); | |
| (envModule.ENV as Record<string, unknown>).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<string, unknown>).BACKEND_URL = "https://backend.example.com"; | |
| await _tickForTest(); // prima esecuzione → cooldown impostato | |
| await _tickForTest(); // seconda esecuzione → h3 in cooldown | |
| (envModule.ENV as Record<string, unknown>).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<string, unknown>).BACKEND_URL = "https://backend.example.com"; | |
| await expect(_tickForTest()).resolves.not.toThrow(); | |
| (envModule.ENV as Record<string, unknown>).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<string, unknown>).BACKEND_URL = "https://backend.example.com"; | |
| await _tickForTest(); // imposta cooldown | |
| clearCooldowns(); // resetta | |
| await _tickForTest(); // ora può ritentare | |
| (envModule.ENV as Record<string, unknown>).BACKEND_URL = origUrl; | |
| expect(mockLoop.run).toHaveBeenCalledTimes(2); | |
| }); | |
| }); | |