Spaces:
Running
Running
| 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"); | |
| }); | |
| }); | |