Spaces:
Sleeping
Sleeping
| /** | |
| * reflectionBlockN5.test.ts — N5: guard basso-budget e direct-answer | |
| * | |
| * Verifica che runReflectionBlock ritorni immediatamente (senza API call) | |
| * quando: | |
| * N5-1/2: effectiveMaxIter ≤ 3 (budget troppo basso per reflection utile) | |
| * N5-3: isDirectAnswer = true (fast-path, 1 iter, nessun benefit) | |
| * | |
| * Tecnica: vi.mock su providerChain con spy che lancia — se N5 guard non | |
| * fosse attiva la chiamata raggiungerebbe callWithFallback e il test fallirebbe. | |
| * | |
| * Regola: guard N5 deve tornare reflected:false SENZA chiamare callWithFallback. | |
| */ | |
| import { describe, it, expect, vi, beforeEach } from "vitest"; | |
| import { runReflectionBlock } from "@/lib/agentLoop/reflectionBlock"; | |
| import type { AgentStep } from "@/lib/storage"; | |
| import type { ApiMsg } from "@/lib/agentLoop/networkTools"; | |
| // ── Mock providerChain ──────────────────────────────────────────────────────── | |
| // callWithFallback lancia — se N5 guard non si attiva il test fallisce | |
| vi.mock("@/lib/providerChain", () => ({ | |
| callWithFallback: vi.fn().mockRejectedValue(new Error("N5-guard-not-fired")), | |
| getHFToken: vi.fn().mockReturnValue(""), | |
| })); | |
| // ── Fixtures ────────────────────────────────────────────────────────────────── | |
| const MSGS: ApiMsg[] = [{ role: "user", content: "query di test" }]; | |
| const STEPS = [ | |
| { id: "1", tool: "web_search", status: "done", result: "risultato1", ts: 0 }, | |
| { id: "2", tool: "read_file", status: "done", result: "risultato2", ts: 0 }, | |
| { id: "3", tool: "github_fetch", status: "error", result: "timeout", ts: 0 }, | |
| ] as unknown as AgentStep[]; | |
| // iter=4 → candidato shouldRun (4 % REFLECTION_EVERY=4 === 0, iter > 0) | |
| const BASE = { | |
| iter: 4, | |
| effectiveMaxIter: 8, // > 3, non direct-answer → shouldRun=true senza N5 | |
| lastUserMsg: "Quali sono le ultime notizie?", | |
| steps: STEPS, | |
| loopMessages: MSGS, | |
| onStatus: vi.fn(), | |
| isDirectAnswer: false, | |
| }; | |
| beforeEach(() => { vi.clearAllMocks(); }); | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| describe("N5 reflectionBlock guard — basso budget", () => { | |
| it("N5-1: skip con effectiveMaxIter=3 (soglia esatta) — callWithFallback non chiamata", async () => { | |
| const { callWithFallback } = await import("@/lib/providerChain"); | |
| const r = await runReflectionBlock({ ...BASE, effectiveMaxIter: 3 }); | |
| expect(r.reflected).toBe(false); | |
| expect(r.loopMessages).toStrictEqual(MSGS); | |
| expect(callWithFallback).not.toHaveBeenCalled(); | |
| }); | |
| it("N5-2: skip con effectiveMaxIter=1 (direct-answer budget) — callWithFallback non chiamata", async () => { | |
| const { callWithFallback } = await import("@/lib/providerChain"); | |
| const r = await runReflectionBlock({ ...BASE, effectiveMaxIter: 1 }); | |
| expect(r.reflected).toBe(false); | |
| expect(callWithFallback).not.toHaveBeenCalled(); | |
| }); | |
| it("N5-3: skip con isDirectAnswer=true (qualunque budget) — callWithFallback non chiamata", async () => { | |
| const { callWithFallback } = await import("@/lib/providerChain"); | |
| const r = await runReflectionBlock({ ...BASE, isDirectAnswer: true, effectiveMaxIter: 10 }); | |
| expect(r.reflected).toBe(false); | |
| expect(callWithFallback).not.toHaveBeenCalled(); | |
| }); | |
| it("N5-4: skip con entrambe le condizioni vere — una sola è sufficiente", async () => { | |
| const { callWithFallback } = await import("@/lib/providerChain"); | |
| const r = await runReflectionBlock({ ...BASE, isDirectAnswer: true, effectiveMaxIter: 2 }); | |
| expect(r.reflected).toBe(false); | |
| expect(callWithFallback).not.toHaveBeenCalled(); | |
| }); | |
| }); | |
| describe("N5 reflectionBlock guard — guard NON attiva (path normale)", () => { | |
| it("N5-5: effectiveMaxIter=4 (> 3) e non directAnswer → guard non attiva → raggiunge callWithFallback", async () => { | |
| const { callWithFallback } = await import("@/lib/providerChain"); | |
| // callWithFallback è mockata per lanciare → catch → reflected:false | |
| // ma la cosa importante è che VIENE CHIAMATA (guard N5 non ha fermato il flusso) | |
| const r = await runReflectionBlock({ ...BASE, effectiveMaxIter: 4 }); | |
| // effectiveMaxIter=4: iter(4) < maxIter-1(3) → FALSE → shouldRun=false → non raggiunge callWithFallback | |
| // Con effectiveMaxIter=5: iter(4) < 4 → TRUE, testa il path callWithFallback | |
| expect(r.reflected).toBe(false); // reflected:false perché shouldRun=false (iter non < maxIter-1) | |
| }); | |
| it("N5-6: effectiveMaxIter=6, iter=4 → shouldRun=true → callWithFallback chiamata (catch → reflected:false)", async () => { | |
| const { callWithFallback } = await import("@/lib/providerChain"); | |
| const r = await runReflectionBlock({ ...BASE, effectiveMaxIter: 6 }); | |
| // shouldRun: iter(4)>0 ✓, 4%4===0 ✓, iter(4)<5 ✓, steps.length(3)≥2 ✓ | |
| // → raggiunge callWithFallback che lancia → catch → reflected:false | |
| expect(r.reflected).toBe(false); | |
| expect(callWithFallback).toHaveBeenCalledOnce(); // N5 guard NON ha fermato il flusso | |
| }); | |
| }); | |