/** * proofLedgerChain.test.ts — Regressione GAP-TRUTH-2 * * Verifica la correttezza della catena proof-ledger dopo la rimozione della * doppia scrittura introdotta erroneamente in toolExecutor.ts (GAP-TRUTH-2). * * BUG documentato (pre-fix): * toolExecutor.ts registrava proofLedger.record({ verified: true }) nel * momento del [✓ VERIFIED] stamp testuale, PRIMA che verifyToolResult() * in executeToolGatedFactory.ts potesse valutare la qualità del risultato. * Risultato: un git_push senza SHA nel result riceveva comunque una entry * { verified: true } nel ledger → checkClaimsAgainstLedger non emetteva * warning → falso silenzio su claim non provate. * * Copertura (7 test): * 1. BUG: doppia scrittura {true, false} → problems=[] (silenzio sbagliato) * 2. FIXED: singola scrittura {false} → problems.length===1 (warning corretto) * 3. Happy path: singola scrittura {true} → problems=[] (silenzio corretto) * 4. Ledger vuoto su claim → problems "nessuna azione registrata" * 5. Doppia categoria: file_write non provata, git_push provata → solo file_write in problems * 6. reset() garantisce isolamento tra run agente * 7. Tutte le categorie: ogni CATEGORY_TO_LEDGER_ACTIONS copre i tool corretti * * SESSIONE: GAP-TRUTH-2 */ import { describe, it, expect, beforeEach } from "vitest"; import { proofLedger } from "@/lib/agentLoop/proofLedger"; import { checkClaimsAgainstLedger } from "@/lib/agentLoop/actionClaimDetector"; import { CATEGORY_TO_LEDGER_ACTIONS, PROOF_VERIFIED_TOOLS, TOOL_TO_CATEGORY_LABEL } from "@/lib/agentLoop/proofToolRegistry"; import type { ClaimCategory } from "@/lib/agentLoop/proofToolRegistry"; // Reset prima di ogni test — fondamentale per isolamento beforeEach(() => { proofLedger.reset(); }); // ─── T1: BUG scenario — documenta la regressione ───────────────────────────── describe("T1 — BUG scenario (doppia scrittura, pre-fix)", () => { it( "verified:true poi verified:false → entries.some(verified)=true → problems=[] (silenzio sbagliato)", () => { // Simula il vecchio toolExecutor: record verified:true al momento dello stamp proofLedger.record({ action: "git_push", verified: true, evidence: "✅ Push completato", // nessun SHA — risultato non verificabile ts: Date.now(), }); // Poi executeToolGatedFactory: verifyToolResult → SHA assente → verified:false proofLedger.record({ action: "git_push", verified: false, evidence: "Push ok ma SHA non trovato nel result", ts: Date.now(), }); const snapshot = proofLedger.snapshot(); expect(snapshot).toHaveLength(2); const claimed = new Set(["git_push"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); // BUG: entries.some(e => e.verified) trova il primo record (true) // → warning non emesso → l'utente NON viene avvertito del push non provato expect(problems).toHaveLength(0); // documenta il comportamento SCORRETTO }, ); }); // ─── T2: FIXED scenario — comportamento corretto post-fix ──────────────────── describe("T2 — FIXED scenario (singola scrittura da verifier)", () => { it( "solo verified:false → entries.some(verified)=false → warning emesso", () => { // Post-fix: solo executeToolGatedFactory registra, con verità verificata proofLedger.record({ action: "git_push", verified: false, evidence: "✅ Push completato — SHA non trovato nel result (proof assente)", ts: Date.now(), }); const snapshot = proofLedger.snapshot(); expect(snapshot).toHaveLength(1); const claimed = new Set(["git_push"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); // CORRETTO: solo entry con verified:false → warning emesso expect(problems).toHaveLength(1); expect(problems[0]).toMatch(/commit\/push su Git/); expect(problems[0]).toMatch(/eseguito ma non verificato/); }, ); it( "verified:true (SHA reale nel result) → problems=[] (silenzio corretto)", () => { proofLedger.record({ action: "git_push", verified: true, evidence: "SHA=a1b2c3d4e5f6 pushed to origin/main", ts: Date.now(), }); const snapshot = proofLedger.snapshot(); const claimed = new Set(["git_push"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); // Prova presente e verificata → silenzio corretto expect(problems).toHaveLength(0); }, ); it( "ledger vuoto per categoria dichiarata → 'nessuna azione registrata'", () => { // Ledger vuoto (reset() già chiamato in beforeEach) const snapshot = proofLedger.snapshot(); expect(snapshot).toHaveLength(0); const claimed = new Set(["deploy"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); expect(problems).toHaveLength(1); expect(problems[0]).toMatch(/deploy/); expect(problems[0]).toMatch(/nessuna azione registrata/); }, ); }); // ─── T3: Scenari multi-categoria ────────────────────────────────────────────── describe("T3 — Multi-categoria", () => { it( "file_write non provata, git_push provata → solo file_write in problems", () => { // git_push verificato proofLedger.record({ action: "git_push", verified: true, evidence: "sha:xyz789", ts: Date.now() }); // write_file non verificato (es. errore ENOENT) proofLedger.record({ action: "write_file", verified: false, evidence: "❌ ENOENT", ts: Date.now() }); const snapshot = proofLedger.snapshot(); const claimed = new Set(["git_push", "file_write"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); expect(problems).toHaveLength(1); expect(problems[0]).toMatch(/scrittura\/modifica file/); expect(problems[0]).not.toMatch(/commit\/push su Git/); }, ); it( "apply_patch verified:true copre la categoria file_write → problems=[]", () => { proofLedger.record({ action: "apply_patch", verified: true, evidence: "patch applied", ts: Date.now() }); const snapshot = proofLedger.snapshot(); const claimed = new Set(["file_write"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); expect(problems).toHaveLength(0); }, ); }); // ─── T4: Isolamento sessione ─────────────────────────────────────────────────── describe("T4 — Isolamento sessione via reset()", () => { it( "reset() garantisce che prove della sessione precedente non contaminino quella corrente", () => { // Sessione 1: registra prove proofLedger.record({ action: "git_push", verified: true, evidence: "sha:aaa", ts: Date.now() }); expect(proofLedger.count()).toBe(1); // Reset (chiamato da agentLoop prima di ogni run) proofLedger.reset(); expect(proofLedger.count()).toBe(0); // Sessione 2: ledger pulito — claim git_push non ha prove const snapshot = proofLedger.snapshot(); const claimed = new Set(["git_push"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); expect(problems).toHaveLength(1); expect(problems[0]).toMatch(/nessuna azione registrata/); }, ); }); // ─── T5: Completezza CATEGORY_TO_LEDGER_ACTIONS ─────────────────────────────── describe("T5 — Completezza registry (ogni tool in PROOF_VERIFIED_TOOLS ha una categoria)", () => { it( "ogni tool in CATEGORY_TO_LEDGER_ACTIONS è coperto da una categoria (no orfani)", () => { const allMapped = Object.values(CATEGORY_TO_LEDGER_ACTIONS).flat(); const cats = Object.keys(CATEGORY_TO_LEDGER_ACTIONS) as ClaimCategory[]; for (const tool of allMapped) { const belongsToCategory = cats.some(cat => CATEGORY_TO_LEDGER_ACTIONS[cat].includes(tool), ); expect(belongsToCategory, `${tool} non appartiene a nessuna categoria`).toBe(true); } }, ); it( "git_push_multiple è nella categoria git_push (verifica alias variante)", () => { proofLedger.record({ action: "git_push_multiple", verified: false, evidence: "auth error", ts: Date.now() }); const snapshot = proofLedger.snapshot(); const claimed = new Set(["git_push"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); // git_push_multiple è nelle azioni di git_push → catturato correttamente expect(problems).toHaveLength(1); expect(problems[0]).toMatch(/eseguito ma non verificato/); }, ); }); // ─── T6: Regressione create_file in PROOF_VERIFIED_TOOLS ────────────────────── // Riproduce il bug dove emitFinalText._CRIT_TOOLS locale mancava create_file. // La 5ª copia locale aveva: write_file, apply_patch, create_project, run_code, // execute_shell, run_python, git_push, git_push_multiple, deploy_trigger (9 tool). // PROOF_VERIFIED_TOOLS ha 10 tool — create_file era il buco. describe("T6 — Regressione: create_file presente in PROOF_VERIFIED_TOOLS", () => { it( "create_file è in PROOF_VERIFIED_TOOLS (non deve mai sparire)", () => { expect(PROOF_VERIFIED_TOOLS.has("create_file")).toBe(true); }, ); it( "create_file non verificato → warning in checkClaimsAgainstLedger", () => { proofLedger.record({ action: "create_file", verified: false, evidence: "write failed", ts: Date.now() }); const snapshot = proofLedger.snapshot(); const claimed = new Set(["file_write"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); expect(problems).toHaveLength(1); expect(problems[0]).toMatch(/eseguito ma non verificato/); }, ); it( "create_file verificato → nessun warning", () => { proofLedger.record({ action: "create_file", verified: true, evidence: "ok:sha123", ts: Date.now() }); const snapshot = proofLedger.snapshot(); const claimed = new Set(["file_write"]); const problems = checkClaimsAgainstLedger(claimed, snapshot); expect(problems).toHaveLength(0); }, ); }); // ─── T7: Validazione TOOL_TO_CATEGORY_LABEL derived map ────────────────────── // Verifica che TOOL_TO_CATEGORY_LABEL (la mappa derivata che ha sostituito _critLabel()) // copra tutti i tool in PROOF_VERIFIED_TOOLS e ritorni label leggibili. describe("T7 — TOOL_TO_CATEGORY_LABEL copertura completa", () => { it( "ogni tool in PROOF_VERIFIED_TOOLS ha un entry in TOOL_TO_CATEGORY_LABEL", () => { for (const tool of PROOF_VERIFIED_TOOLS) { const label = TOOL_TO_CATEGORY_LABEL.get(tool); expect(label, `${tool} non ha label in TOOL_TO_CATEGORY_LABEL`).toBeDefined(); expect(label!.length, `${tool} ha label vuota`).toBeGreaterThan(0); } }, ); it( "git_push → 'commit/push su Git' · write_file → 'scrittura/modifica file' · deploy_trigger → 'deploy'", () => { expect(TOOL_TO_CATEGORY_LABEL.get("git_push")).toBe("commit/push su Git"); expect(TOOL_TO_CATEGORY_LABEL.get("write_file")).toBe("scrittura/modifica file"); expect(TOOL_TO_CATEGORY_LABEL.get("deploy_trigger")).toBe("deploy"); expect(TOOL_TO_CATEGORY_LABEL.get("create_file")).toBe("scrittura/modifica file"); }, ); it( "TOOL_TO_CATEGORY_LABEL dimensione = PROOF_VERIFIED_TOOLS dimensione (bijection check)", () => { // Ogni tool in PROOF_VERIFIED_TOOLS deve avere esattamente 1 label (no duplicati, no orfani). expect(TOOL_TO_CATEGORY_LABEL.size).toBe(PROOF_VERIFIED_TOOLS.size); }, ); });