Files
waggle-os/packages/agent/tests/skill-recommender.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

115 lines
5.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { SkillRecommender, type SkillRecommenderDeps } from '../src/skill-recommender.js';
function makeDeps(
skills: Array<{ name: string; content: string }>,
activeSkills?: string[],
): SkillRecommenderDeps {
return {
getSkills: () => skills,
activeSkills,
};
}
describe('SkillRecommender', () => {
const sampleSkills = [
{ name: 'code-review', content: 'Guidelines for reviewing pull requests. Check style, logic errors, and test coverage.' },
{ name: 'writing-style', content: 'Maintain a professional tone. Use active voice. Avoid jargon.' },
{ name: 'data-analysis', content: 'Steps for analyzing datasets: clean data, explore patterns, visualize results, report findings.' },
{ name: 'project-planning', content: 'Break work into milestones. Estimate effort. Track dependencies and risks.' },
];
it('returns recommendations when skills match context keywords', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills));
const results = rec.recommend('I need help reviewing code for this pull request');
expect(results.length).toBeGreaterThan(0);
expect(results[0].skillName).toBe('code-review');
expect(results[0].relevanceScore).toBeGreaterThan(0);
expect(results[0].reason).toBeTruthy();
});
it('name matches score higher than content-only matches', () => {
const skills = [
{ name: 'analysis-helper', content: 'Generic helper skill with no special keywords.' },
{ name: 'generic-skill', content: 'This skill helps with analysis of data and patterns.' },
];
const rec = new SkillRecommender(makeDeps(skills));
// Use multiple keywords so the 2x name-match weight creates a gap
const results = rec.recommend('analysis helper tool');
expect(results.length).toBe(2);
// Name match should score higher (name hits "analysis" and "helper" at 2x each)
expect(results[0].skillName).toBe('analysis-helper');
expect(results[0].relevanceScore).toBeGreaterThan(results[1].relevanceScore);
});
it('filters out active skills', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills, ['code-review']));
const results = rec.recommend('review code pull request');
const names = results.map(r => r.skillName);
expect(names).not.toContain('code-review');
});
it('returns empty array when no skills match', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills));
const results = rec.recommend('quantum physics entanglement');
expect(results).toEqual([]);
});
it('respects topN limit', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills));
// Use a broad context that could match multiple skills
const results = rec.recommend('review code data analysis planning style', 2);
expect(results.length).toBeLessThanOrEqual(2);
});
it('case-insensitive matching', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills));
const lower = rec.recommend('code review');
const upper = rec.recommend('CODE REVIEW');
expect(lower.length).toBe(upper.length);
expect(lower[0].skillName).toBe(upper[0].skillName);
expect(lower[0].relevanceScore).toBe(upper[0].relevanceScore);
});
it('handles empty context gracefully', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills));
expect(rec.recommend('')).toEqual([]);
expect(rec.recommend(' ')).toEqual([]);
});
it('minimum score threshold filters noise', () => {
// Skill with content that barely overlaps — lots of keywords, only one match
const skills = [
{ name: 'xyz-unrelated', content: 'This mentions code once but nothing else relevant.' },
];
const rec = new SkillRecommender(makeDeps(skills));
// Many keywords, "code" matches but score will be low
const results = rec.recommend(
'alpha beta gamma delta epsilon zeta eta theta iota kappa lambda code',
);
// "code" matches in content = 1 point, 12 keywords total => score ~0.083 < 0.1
// Some stop words filter out, let's check: alpha(ok) beta(ok) gamma(ok) delta(ok)
// epsilon(ok) zeta(ok) eta(ok=3chars) theta(ok) iota(ok=4chars) kappa(ok) lambda(ok) code(ok)
// That's 12 keywords, 1 match => 0.083 => below threshold
expect(results).toEqual([]);
});
it('returns reason describing name match', () => {
const rec = new SkillRecommender(makeDeps(sampleSkills));
const results = rec.recommend('code review');
const codeReview = results.find(r => r.skillName === 'code-review');
expect(codeReview).toBeDefined();
expect(codeReview!.reason).toContain('Skill name matches');
});
it('returns reason describing content match', () => {
const skills = [
{ name: 'my-skill', content: 'This skill teaches visualization techniques for charts.' },
];
const rec = new SkillRecommender(makeDeps(skills));
const results = rec.recommend('visualization charts');
expect(results.length).toBe(1);
expect(results[0].reason).toContain('Skill content mentions');
});
});