Spaces:
Sleeping
Sleeping
| /** | |
| * 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); | |
| }); | |
| }); | |