141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
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);
|
|
});
|
|
});
|