Spaces:
Running
Running
| /** | |
| * 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<typeof makeMockDb>; | |
| 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([]); | |
| }); | |
| }); | |