/** * skillPersistence.test.ts — Unit test S32 * * Copre: * - recordPattern: nuovo pattern → {successCount:1, totalCount:1, confidence:1} * - recordPattern: pattern esistente → aggiorna successCount/totalCount/confidence * - recordPattern: success=false → confidence < 1 * - recordPattern: toolSequence troncata a MAX_SEQUENCE_LEN (8) * - recordPattern: taskSignature normalizzata (lowercase, punteggiatura rimossa) * - suggestPattern: nessun pattern → null * - suggestPattern: pattern sotto soglia successCount → null * - suggestPattern: pattern sotto soglia confidence → null * - suggestPattern: task dissimile (Jaccard < 0.25) → null * - suggestPattern: task simile + soglie ok → stringa hint * - suggestPattern: hint contiene toolSequence e % * - suggestPattern: hint ≤ 300 chars * - getPatterns: ritorna tutti i pattern in memoria * - clearPatterns: svuota la memoria * - initSkillPersistence: carica da DB mock * - _setTestDb: sostituisce Dexie reale con mock */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { recordPattern, suggestPattern, getPatterns, clearPatterns, initSkillPersistence, _setTestDb, _resetDb, type SkillPattern, } from "@/lib/skillPersistence"; // ─── Mock DB factory ────────────────────────────────────────────────────────── function makeMockDb(initialRows: SkillPattern[] = []) { const rows: SkillPattern[] = [...initialRows]; return { patterns: { put: vi.fn().mockResolvedValue(undefined), clear: vi.fn().mockResolvedValue(undefined), toArray: vi.fn().mockResolvedValue(rows), }, }; } // ─── Setup / Teardown ───────────────────────────────────────────────────────── let mockDb: ReturnType; beforeEach(() => { mockDb = makeMockDb(); _setTestDb(mockDb); clearPatterns(); }); afterEach(() => { _resetDb(); clearPatterns(); }); // ─── recordPattern ──────────────────────────────────────────────────────────── describe("recordPattern", () => { it("nuovo pattern success → successCount=1, totalCount=1, confidence=1", () => { const p = recordPattern("cerca meteo Roma", ["web_search", "read_page"], true); expect(p.successCount).toBe(1); expect(p.totalCount).toBe(1); expect(p.confidence).toBe(1); expect(p.toolSequence).toEqual(["web_search", "read_page"]); }); it("nuovo pattern failure → successCount=0, confidence=0", () => { const p = recordPattern("cerca meteo Roma", ["web_search"], false); expect(p.successCount).toBe(0); expect(p.confidence).toBe(0); }); it("pattern esistente aggiornato: 2 success → confidence=1", () => { recordPattern("scrivi file TypeScript", ["write_file"], true); const p = recordPattern("scrivi file TypeScript", ["write_file"], true); expect(p.successCount).toBe(2); expect(p.totalCount).toBe(2); expect(p.confidence).toBe(1); }); it("1 success + 1 failure → confidence=0.5", () => { recordPattern("debug codice", ["run_code", "read_file"], true); const p = recordPattern("debug codice", ["run_code", "read_file"], false); expect(p.totalCount).toBe(2); expect(p.confidence).toBe(0.5); }); it("toolSequence troncata a 8 elementi", () => { const tools = ["a","b","c","d","e","f","g","h","i","j"]; const p = recordPattern("task lungo", tools, true); expect(p.toolSequence.length).toBe(8); expect(p.toolSequence).toEqual(["a","b","c","d","e","f","g","h"]); }); it("taskSignature normalizzata: lowercase e punteggiatura rimossa", () => { const p = recordPattern("CERCA! meteo a Roma???", ["web_search"], true); expect(p.taskSignature).not.toContain("!"); expect(p.taskSignature).not.toContain("?"); expect(p.taskSignature).toBe(p.taskSignature.toLowerCase()); }); it("stesso task (normalizzato) → stesso id", () => { const p1 = recordPattern("cerca meteo Roma", ["web_search"], true); const p2 = recordPattern("CERCA METEO ROMA", ["web_search"], false); expect(p1.id).toBe(p2.id); }); it("salva su DB mock (write-behind)", () => { recordPattern("test task", ["web_search"], true); expect(mockDb.patterns.put).toHaveBeenCalledTimes(1); }); it("aggiorna lastUsed al timestamp corrente", () => { const before = Date.now(); const p = recordPattern("task test", ["web_search"], true); expect(p.lastUsed).toBeGreaterThanOrEqual(before); }); }); // ─── suggestPattern ─────────────────────────────────────────────────────────── describe("suggestPattern", () => { it("nessun pattern → null", () => { expect(suggestPattern("cerca meteo Roma")).toBeNull(); }); it("pattern con successCount < 2 → null", () => { recordPattern("cerca meteo Roma", ["web_search"], true); // successCount=1 expect(suggestPattern("cerca meteo Roma")).toBeNull(); }); it("pattern con confidence < 0.6 → null", () => { recordPattern("cerca meteo Roma", ["web_search"], true); // 1/2 recordPattern("cerca meteo Roma", ["web_search"], false); // confidence=0.5 expect(suggestPattern("cerca meteo Roma")).toBeNull(); }); it("task completamente diverso (Jaccard bassa) → null", () => { // Registro pattern per "meteo Roma" recordPattern("meteo Roma oggi", ["web_search"], true); recordPattern("meteo Roma oggi", ["web_search"], true); // Cerco qualcosa di completamente diverso expect(suggestPattern("scrivi codice Python per ordinare lista")).toBeNull(); }); it("task simile + soglie ok → stringa hint non null", () => { recordPattern("cerca meteo Roma", ["web_search", "read_page"], true); recordPattern("cerca meteo Roma", ["web_search", "read_page"], true); const hint = suggestPattern("meteo Roma previsioni"); expect(hint).not.toBeNull(); expect(typeof hint).toBe("string"); }); it("hint contiene la sequenza tool", () => { recordPattern("scrivi file ts", ["write_file", "run_code"], true); recordPattern("scrivi file ts", ["write_file", "run_code"], true); const hint = suggestPattern("scrivi file typescript"); expect(hint).toContain("write_file"); expect(hint).toContain("run_code"); }); it("hint contiene la percentuale di successo", () => { recordPattern("cerca notizie", ["web_search"], true); recordPattern("cerca notizie", ["web_search"], true); const hint = suggestPattern("notizie recenti"); expect(hint).toContain("100%"); }); it("hint ≤ 300 chars", () => { const longTools = Array(8).fill("web_search_tool_very_long_name"); recordPattern("cerca meteo Roma", longTools, true); recordPattern("cerca meteo Roma", longTools, true); const hint = suggestPattern("meteo Roma oggi"); if (hint) expect(hint.length).toBeLessThanOrEqual(300); }); it("hint contiene SKILL NOTO", () => { recordPattern("scrivi codice", ["run_code"], true); recordPattern("scrivi codice", ["run_code"], true); const hint = suggestPattern("scrivi codice typescript"); if (hint) expect(hint).toContain("SKILL NOTO"); }); }); // ─── getPatterns / clearPatterns ────────────────────────────────────────────── describe("getPatterns / clearPatterns", () => { it("getPatterns restituisce array vuoto inizialmente", () => { expect(getPatterns()).toEqual([]); }); it("getPatterns ritorna i pattern registrati", () => { recordPattern("task A", ["web_search"], true); recordPattern("task B", ["run_code"], false); expect(getPatterns().length).toBe(2); }); it("clearPatterns svuota la memoria", () => { recordPattern("task A", ["web_search"], true); clearPatterns(); expect(getPatterns()).toEqual([]); }); }); // ─── initSkillPersistence ───────────────────────────────────────────────────── describe("initSkillPersistence", () => { it("carica pattern dal DB mock in memoria", async () => { const stored: SkillPattern = { id: "abc", taskSignature: "test task", toolSequence: ["web_search"], successCount: 3, totalCount: 3, lastUsed: Date.now(), confidence: 1, }; const db = makeMockDb([stored]); _setTestDb(db); clearPatterns(); await initSkillPersistence(); const patterns = getPatterns(); expect(patterns.length).toBe(1); expect(patterns[0].id).toBe("abc"); }); it("non crasha se DB lancia", async () => { const db = makeMockDb(); db.patterns.toArray = vi.fn().mockRejectedValue(new Error("DB error")); _setTestDb(db); clearPatterns(); await expect(initSkillPersistence()).resolves.not.toThrow(); expect(getPatterns()).toEqual([]); }); });