115 lines
5.0 KiB
TypeScript
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');
|
|
});
|
|
});
|