import { describe, expect, it } from "vitest"; import { computeAvailabilityScore, computeCostScore, computeSkillScore, computeUtilizationScore, rankResources, } from "../skill-matcher.js"; const mockSkills = [ { skill: "TypeScript", proficiency: 5 as const, yearsExperience: 5 }, { skill: "React", proficiency: 4 as const, yearsExperience: 3 }, { skill: "Node.js", proficiency: 3 as const, yearsExperience: 2 }, ]; describe("computeSkillScore", () => { it("returns 100 for no required skills", () => { const result = computeSkillScore(mockSkills, []); expect(result.score).toBe(100); }); it("scores correctly for full match with max proficiency", () => { const result = computeSkillScore(mockSkills, ["TypeScript"]); // TypeScript proficiency 5/5 = 1.0, required portion = 70%, preferred = 30% (none) expect(result.score).toBeGreaterThan(90); expect(result.matchedRequired).toContain("TypeScript"); expect(result.missing).toHaveLength(0); }); it("scores lower for missing required skills", () => { const result = computeSkillScore(mockSkills, ["Python"]); expect(result.score).toBeLessThan(50); expect(result.missing).toContain("Python"); }); it("matches skills case-insensitively", () => { const result = computeSkillScore(mockSkills, ["typescript"]); expect(result.matchedRequired).toHaveLength(1); }); it("accounts for preferred skills: matched preferred yields high score", () => { const withPref = computeSkillScore(mockSkills, ["TypeScript"], ["React"]); const withoutPref = computeSkillScore(mockSkills, ["TypeScript"], []); // Both should score high; preferred matching is reflected in breakdown expect(withPref.score).toBeGreaterThanOrEqual(80); expect(withoutPref.score).toBe(100); expect(withPref.matchedPreferred).toContain("React"); }); }); describe("computeAvailabilityScore", () => { it("returns 100 for no conflicts", () => { expect(computeAvailabilityScore(false)).toBe(100); }); it("reduces score by 10 per conflict day", () => { expect(computeAvailabilityScore(true, 5)).toBe(50); }); it("floors at 0", () => { expect(computeAvailabilityScore(true, 15)).toBe(0); }); }); describe("computeCostScore", () => { it("returns 50 for no budget defined", () => { expect(computeCostScore(10000)).toBe(50); }); it("returns 100 when resource cost is at budget", () => { expect(computeCostScore(10000, 10000)).toBe(100); }); it("returns 100 when resource cost is below budget", () => { expect(computeCostScore(8000, 10000)).toBe(100); }); it("penalizes resources above budget", () => { const score = computeCostScore(15000, 10000); // 1.5x budget expect(score).toBeLessThan(100); expect(score).toBeGreaterThanOrEqual(0); }); }); describe("computeUtilizationScore", () => { it("returns 100 for well below target (20+ gap)", () => { expect(computeUtilizationScore(50, 80)).toBe(100); }); it("returns 0 for severely overallocated", () => { expect(computeUtilizationScore(110, 80)).toBe(0); }); }); describe("rankResources", () => { const resources = [ { id: "r1", displayName: "Alice", eid: "EMP-001", skills: mockSkills, lcrCents: 8000, chargeabilityTarget: 80, currentUtilizationPercent: 50, hasAvailabilityConflicts: false, }, { id: "r2", displayName: "Bob", eid: "EMP-002", skills: [{ skill: "Python", proficiency: 4 as const }], lcrCents: 12000, chargeabilityTarget: 80, currentUtilizationPercent: 90, hasAvailabilityConflicts: true, conflictDays: ["2025-01-06"], }, ]; it("returns suggestions sorted by score descending", () => { const results = rankResources({ requiredSkills: ["TypeScript"], resources, }); expect(results[0]?.score).toBeGreaterThanOrEqual(results[1]?.score ?? 0); }); it("Alice ranks higher than Bob for TypeScript requirement", () => { const results = rankResources({ requiredSkills: ["TypeScript"], resources, }); expect(results[0]?.resourceId).toBe("r1"); }); it("returns all resources", () => { const results = rankResources({ requiredSkills: [], resources }); expect(results).toHaveLength(2); }); });