/** * debugLoop.test.ts — Unit test S26 * * Testa il ciclo di esecuzione + auto-fix: * - run: successo al primo tentativo * - run: fallisce → llm_fix → riprova → successo * - run: esaurisce i retry → fallisce * - run: fix uguale al codice originale → break anticipato * - Emissioni eventi: agent_step_start, agent_step_complete, agent_step_error * - Callbacks: onFix, onSuccess * * Mock: fetch (globale), @/core/events, @/config/env */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("@/core/events", () => ({ eventRuntime: { emit: vi.fn() }, })); vi.mock("@/config/env", () => ({ ENV: { BACKEND_URL: "http://localhost:8000" }, })); import { debugLoop } from "@/lib/debugLoop"; import { eventRuntime } from "@/core/events"; const mockEmit = vi.mocked(eventRuntime.emit); // ─── Helpers fetch ──────────────────────────────────────────────────────────── function execOk(stdout = "Hello"): Response { // S770-Fix2: backend sends exit_code (snake_case), not exitCode return { ok: true, json: async () => ({ stdout, stderr: "", exit_code: 0, durationMs: 5 }) } as unknown as Response; } function execFail(stderr = "ReferenceError: x is not defined"): Response { // S770-Fix2: backend sends exit_code (snake_case) return { ok: true, json: async () => ({ stdout: "", stderr, exit_code: 1, durationMs: 3 }) } as unknown as Response; } function llmFix(code: string): Response { return { ok: true, json: async () => ({ fixed_code: code }) } as unknown as Response; } beforeEach(() => vi.clearAllMocks()); afterEach(() => vi.unstubAllGlobals()); // ─── successo immediato ─────────────────────────────────────────────────────── describe("debugLoop.run — successo immediato", () => { it("ritorna success=true, attempts=1 se exec ha exitCode=0", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(execOk("output line 1"))); const result = await debugLoop.run("console.log('hello')", { taskId: "t-001", lang: "javascript", }); expect(result.success).toBe(true); expect(result.attempts).toBe(1); expect(result.lastResult?.exitCode).toBe(0); expect(result.lastResult?.stdout).toBe("output line 1"); expect(result.finalCode).toBe("console.log('hello')"); }); it("emette agent_step_start + agent_step_complete al successo", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(execOk())); await debugLoop.run("print(1)", { taskId: "t-002", lang: "python" }); expect(mockEmit).toHaveBeenCalledWith( expect.objectContaining({ kind: "agent_step_start", taskId: "t-002", step: 1 }), ); expect(mockEmit).toHaveBeenCalledWith( expect.objectContaining({ kind: "agent_step_complete", success: true }), ); }); it("chiama onSuccess con il risultato dell'exec", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(execOk("42"))); const onSuccess = vi.fn(); await debugLoop.run("print(42)", { taskId: "t-003", lang: "python", onSuccess }); expect(onSuccess).toHaveBeenCalledOnce(); expect(onSuccess).toHaveBeenCalledWith( expect.objectContaining({ stdout: "42", exitCode: 0 }), ); }); }); // ─── fallisce → llm fix → successo ─────────────────────────────────────────── describe("debugLoop.run — fail → fix → successo", () => { it("riprova con il codice fixato e ritorna success=true, attempts=2", async () => { const fixedCode = "const x = 1; console.log(x)"; vi.stubGlobal("fetch", vi.fn() .mockResolvedValueOnce(execFail("ReferenceError: x is not defined")) // exec 1 → fail .mockResolvedValueOnce(llmFix(fixedCode)) // llm_fix .mockResolvedValueOnce(execOk("1"))); // exec 2 → ok const result = await debugLoop.run("console.log(x)", { taskId: "t-010", lang: "javascript", }); expect(result.success).toBe(true); expect(result.attempts).toBe(2); expect(result.finalCode).toBe(fixedCode); }); it("chiama onFix con attempt e codice fixato", async () => { const fixedCode = "print('fixed')"; const onFix = vi.fn(); vi.stubGlobal("fetch", vi.fn() .mockResolvedValueOnce(execFail("NameError: name 'oops' is not defined")) .mockResolvedValueOnce(llmFix(fixedCode)) .mockResolvedValueOnce(execOk("fixed"))); await debugLoop.run("print(oops)", { taskId: "t-011", lang: "python", onFix }); expect(onFix).toHaveBeenCalledWith(1, fixedCode); }); it("emette agent_step_error al fallimento prima del fix", async () => { const fixedCode = "const x = 1;"; vi.stubGlobal("fetch", vi.fn() .mockResolvedValueOnce(execFail("SyntaxError")) .mockResolvedValueOnce(llmFix(fixedCode)) .mockResolvedValueOnce(execOk())); await debugLoop.run("const = 1;", { taskId: "t-012", lang: "typescript" }); expect(mockEmit).toHaveBeenCalledWith( expect.objectContaining({ kind: "agent_step_error", taskId: "t-012" }), ); }); }); // ─── esaurisce i retry ──────────────────────────────────────────────────────── describe("debugLoop.run — esaurisce retry", () => { it("ritorna success=false dopo maxRetries tentativi falliti", async () => { // exec sempre fallisce, llm_fix ritorna codice diverso ogni volta vi.stubGlobal("fetch", vi.fn() .mockResolvedValueOnce(execFail("err1")) .mockResolvedValueOnce(llmFix("code_v2")) .mockResolvedValueOnce(execFail("err2")) .mockResolvedValueOnce(llmFix("code_v3")) .mockResolvedValueOnce(execFail("err3")) .mockResolvedValueOnce(llmFix("code_v4")) .mockResolvedValueOnce(execFail("err4"))); // attempt oltre maxRetries const result = await debugLoop.run("broken code", { taskId: "t-020", lang: "javascript", maxRetries: 3, }); expect(result.success).toBe(false); expect(result.lastResult?.exitCode).toBe(1); }); it("maxRetries=0 → un solo tentativo, nessun fix", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(execFail("error"))); const onFix = vi.fn(); const result = await debugLoop.run("bad code", { taskId: "t-021", lang: "python", maxRetries: 0, onFix, }); expect(result.success).toBe(false); expect(result.attempts).toBe(1); expect(onFix).not.toHaveBeenCalled(); }); }); // ─── break anticipato (fix invariato) ──────────────────────────────────────── describe("debugLoop.run — break se fix = codice originale", () => { it("esce dal loop se llm_fix ritorna lo stesso codice (nessun progresso)", async () => { const code = "console.log(undefined_var)"; vi.stubGlobal("fetch", vi.fn() .mockResolvedValueOnce(execFail("ReferenceError")) .mockResolvedValueOnce(llmFix(code))); // stessa stringa → break const result = await debugLoop.run(code, { taskId: "t-030", lang: "javascript", }); expect(result.success).toBe(false); // S697: semantic repair (3° chiamata): exec → llm_fix → semantic llm_fix // (il semantic repair è best-effort anche quando fix === codice originale) expect(vi.mocked(fetch)).toHaveBeenCalledTimes(3); }); }); // ─── resilienza fetch ───────────────────────────────────────────────────────── describe("debugLoop.run — resilienza", () => { it("gestisce fetch che lancia eccezione (network error)", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network failure"))); const result = await debugLoop.run("code", { taskId: "t-040", lang: "javascript", maxRetries: 0, }); // Deve ritornare senza crash, con exitCode non 0 expect(result.success).toBe(false); }); });