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