import { describe, it, expect, beforeEach } from "vitest"; // Step4: Procedural Memory — getStrategyFor() unit tests // Tests against a minimal mock of SelfLearningEngine to isolate the logic. interface Lesson { q: string; a: string; kw: string[]; uses: number; score: number; toolSequence?: string[]; } const STOPWORDS = new Set(["il","la","di","a","e","che","non","per","con","in","un","una","dei"]); function extractKeywords(text: string): string[] { return text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/) .filter(w => w.length > 2 && !STOPWORDS.has(w)).slice(0, 40); } function keywordSimilarity(kw1: string[], kw2: string[]): number { if (!kw1.length || !kw2.length) return 0; const set1 = new Set(kw1); const intersection = kw2.filter(k => set1.has(k)).length; return intersection / Math.max(set1.size, kw2.length); } function getStrategyFor(goal: string, lessons: Lesson[]): string | null { const kw = extractKeywords(goal); const withSeq = lessons.filter(l => l.toolSequence && l.toolSequence.length > 0); if (!withSeq.length) return null; const matches = withSeq .map(l => ({ l, sim: keywordSimilarity(kw, l.kw) })) .filter(x => x.sim > 0.4) .sort((a, b) => b.sim * b.l.score - a.sim * a.l.score); if (!matches.length) return null; const { l } = matches[0]; return `[STRATEGIA SUGGERITA (validata ${l.uses}x, score ${Math.round(l.score*100)}%): usa sequenza ${l.toolSequence!.join(" → ")}]`; } const BASE_LESSONS: Lesson[] = [ { q: "push file su github", a: "ok", kw: ["push","file","github"], uses: 5, score: 0.9, toolSequence: ["git_push","check_deploy"] }, { q: "aggiorna readme markdown", a: "ok", kw: ["aggiorna","readme","markdown"], uses: 3, score: 0.8, toolSequence: ["write_file","git_push"] }, { q: "test vitest unit", a: "ok", kw: ["test","vitest","unit"], uses: 2, score: 0.7, toolSequence: ["run_tests"] }, { q: "cerca notizie web", a: "ok", kw: ["cerca","notizie","web"], uses: 4, score: 0.85 }, // no toolSequence ]; describe("getStrategyFor (Step4 – Procedural Memory)", () => { it("returns null when no lessons have toolSequence", () => { const lessons = [{ q:"q", a:"a", kw:["x"], uses:1, score:0.8 }]; expect(getStrategyFor("test", lessons)).toBeNull(); }); it("returns null when no lessons match the goal (sim < 0.4)", () => { expect(getStrategyFor("cucina italiana pasta", BASE_LESSONS)).toBeNull(); }); it("returns strategy hint for matching goal (github push)", () => { const hint = getStrategyFor("push file su github repo", BASE_LESSONS); expect(hint).toMatch(/git_push/); expect(hint).toMatch(/check_deploy/); }); it("returns null for lesson without toolSequence even if keyword match is high", () => { const lessons: Lesson[] = [ { q:"cerca notizie web", a:"ok", kw:["cerca","notizie","web"], uses:10, score:0.95 }, ]; expect(getStrategyFor("cerca notizie web", lessons)).toBeNull(); }); it("hint includes usage count", () => { const hint = getStrategyFor("push file github", BASE_LESSONS); expect(hint).toContain("5x"); }); it("hint includes score percentage", () => { const hint = getStrategyFor("push file github", BASE_LESSONS); expect(hint).toContain("90%"); }); it("prefers higher-scoring match over lower-scoring when both qualify", () => { const lessons: Lesson[] = [ { q:"github push", a:"a", kw:["github","push"], uses:1, score:0.5, toolSequence:["A","B"] }, { q:"github push best", a:"b", kw:["github","push"], uses:1, score:0.95, toolSequence:["C","D"] }, ]; const hint = getStrategyFor("github push", lessons); expect(hint).toContain("C → D"); }); it("uses → separator in tool sequence", () => { const hint = getStrategyFor("push github", BASE_LESSONS); expect(hint).toContain("→"); }); it("returns null for empty lessons array", () => { expect(getStrategyFor("anything", [])).toBeNull(); }); it("returns null for empty goal string", () => { expect(getStrategyFor("", BASE_LESSONS)).toBeNull(); }); it("formats hint with STRATEGIA SUGGERITA prefix", () => { const hint = getStrategyFor("push file github", BASE_LESSONS); expect(hint).toContain("[STRATEGIA SUGGERITA"); }); });