test(application): add 17 dashboard use-case tests for untested queries

Phase 3b Tier 2: covers skill gaps, project health, top value resources,
and peak times dashboard queries including empty data edge cases,
filtering logic, and authorization guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:54:23 +02:00
parent 800a4c5fff
commit cd645c7d55
@@ -0,0 +1,719 @@
import { describe, expect, it, vi } from "vitest";
import {
getDashboardSkillGaps,
getDashboardSkillGapSummary,
getDashboardTopValueResources,
getDashboardProjectHealth,
getDashboardPeakTimes,
} from "../index.js";
// ---------------------------------------------------------------------------
// getDashboardSkillGaps
// ---------------------------------------------------------------------------
describe("getDashboardSkillGaps", () => {
it("computes gaps between open demands and skilled resources", async () => {
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue([
{
role: "Compositor",
roleId: "role_comp",
roleEntity: { name: "Compositor" },
headcount: 3,
metadata: null,
},
{
role: "FX Artist",
roleId: "role_fx",
roleEntity: { name: "FX Artist" },
headcount: 2,
metadata: null,
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
skills: [
{ name: "Compositor", level: 4 },
{ name: "FX Artist", level: 2 },
],
},
{ skills: [{ name: "Compositor", level: 3 }] },
]),
},
};
const result = await getDashboardSkillGaps(db as never);
// FX Artist: demand 2, supply 0 (level 2 < 3) → gap -2 (largest shortage first)
// Compositor: demand 3, supply 2 → gap -1
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ skill: "FX Artist", demand: 2, supply: 0, gap: -2 });
expect(result[1]).toEqual({ skill: "Compositor", demand: 3, supply: 2, gap: -1 });
});
it("returns empty array when there are no open demands", async () => {
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
resource: {
findMany: vi.fn(),
},
};
const result = await getDashboardSkillGaps(db as never);
expect(result).toEqual([]);
// resource.findMany should never be called when there are no demands
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("extracts required skills from demand metadata when present", async () => {
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue([
{
role: "Generalist",
roleId: "role_gen",
roleEntity: { name: "Generalist" },
headcount: 2,
metadata: { requiredSkills: ["Houdini", "Nuke"] },
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{ skills: [{ name: "Houdini", level: 5 }] },
{
skills: [
{ name: "Nuke", level: 3 },
{ name: "Houdini", level: 3 },
],
},
]),
},
};
const result = await getDashboardSkillGaps(db as never);
// Both skills demanded: 2 each
// Houdini supply: 2, gap 0; Nuke supply: 1, gap -1
const nukeRow = result.find((row) => row.skill === "Nuke");
const houdiniRow = result.find((row) => row.skill === "Houdini");
expect(nukeRow).toEqual({ skill: "Nuke", demand: 2, supply: 1, gap: -1 });
expect(houdiniRow).toEqual({ skill: "Houdini", demand: 2, supply: 2, gap: 0 });
// Sorted by gap ascending → Nuke first
expect(result[0]?.skill).toBe("Nuke");
});
it("caps output at 10 rows sorted by largest shortage first", async () => {
const manyDemands = Array.from({ length: 12 }, (_, i) => ({
role: `Role_${i}`,
roleId: `role_${i}`,
roleEntity: { name: `Role_${i}` },
headcount: i + 1,
metadata: null,
}));
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue(manyDemands),
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const result = await getDashboardSkillGaps(db as never);
expect(result).toHaveLength(10);
// All gaps are negative (no supply); sorted by ascending gap → largest demand first
for (let i = 0; i < result.length - 1; i++) {
expect(result[i]?.gap ?? 0).toBeLessThanOrEqual(result[i + 1]?.gap ?? 0);
}
});
});
// ---------------------------------------------------------------------------
// getDashboardSkillGapSummary
// ---------------------------------------------------------------------------
describe("getDashboardSkillGapSummary", () => {
it("returns role gaps, open positions total, skill supply and resource-by-role counts", async () => {
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue([
{
role: "Compositor",
headcount: 3,
roleEntity: { name: "Compositor" },
_count: { assignments: 1 },
},
{
role: "Compositor",
headcount: 2,
roleEntity: { name: "Compositor" },
_count: { assignments: 2 },
},
{
role: "FX Artist",
headcount: 2,
roleEntity: { name: "FX Artist" },
_count: { assignments: 0 },
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
skills: [{ name: "Houdini" }, { skill: "Nuke" }],
areaRole: { name: "FX Artist" },
},
{
skills: [{ name: "Houdini" }],
areaRole: { name: "Compositor" },
},
]),
},
};
const result = await getDashboardSkillGapSummary(db as never);
// Compositor: needed=5, filled=min(1,3)+min(2,2)=1+2=3, gap=2
// FX Artist: needed=2, filled=0, gap=2
expect(result.roleGaps).toHaveLength(2);
expect(result.totalOpenPositions).toBe(4);
const compositorGap = result.roleGaps.find((row) => row.role === "Compositor");
expect(compositorGap).toMatchObject({ needed: 5, filled: 3, gap: 2, fillRate: 60 });
const fxGap = result.roleGaps.find((row) => row.role === "FX Artist");
expect(fxGap).toMatchObject({ needed: 2, filled: 0, gap: 2, fillRate: 0 });
// Skill supply: "houdini" appears twice
expect(result.skillSupplyTop10[0]).toEqual({ skill: "houdini", resourceCount: 2 });
// Resources by role
expect(result.resourcesByRole).toContainEqual({ role: "FX Artist", count: 1 });
expect(result.resourcesByRole).toContainEqual({ role: "Compositor", count: 1 });
});
it("returns empty role gaps and zero open positions when all demands are filled", async () => {
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue([
{
role: "Compositor",
headcount: 2,
roleEntity: { name: "Compositor" },
_count: { assignments: 3 }, // more than headcount → capped at headcount
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const result = await getDashboardSkillGapSummary(db as never);
expect(result.roleGaps).toEqual([]);
expect(result.totalOpenPositions).toBe(0);
});
it("returns empty summary when no demands exist", async () => {
const db = {
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const result = await getDashboardSkillGapSummary(db as never);
expect(result.roleGaps).toEqual([]);
expect(result.totalOpenPositions).toBe(0);
expect(result.skillSupplyTop10).toEqual([]);
expect(result.resourcesByRole).toEqual([]);
});
});
// ---------------------------------------------------------------------------
// getDashboardTopValueResources
// ---------------------------------------------------------------------------
describe("getDashboardTopValueResources", () => {
it("returns resources with value scores for authorised roles", async () => {
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue({
scoreVisibleRoles: ["ADMIN", "MANAGER"],
}),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
eid: "E001",
displayName: "Alice",
chapter: "CGI",
valueScore: 95,
valueScoreBreakdown: {
skillDepth: 20,
skillBreadth: 15,
costEfficiency: 20,
chargeability: 20,
experience: 20,
total: 95,
},
valueScoreUpdatedAt: new Date("2026-01-01T00:00:00.000Z"),
lcrCents: 80_000,
country: { code: "DE", name: "Germany" },
federalState: "BY",
metroCity: { name: "Munich" },
},
]),
},
};
const result = await getDashboardTopValueResources(db as never, {
limit: 10,
userRole: "ADMIN",
});
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: "res_1",
eid: "E001",
displayName: "Alice",
chapter: "CGI",
valueScore: 95,
lcrCents: 80_000,
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Munich",
});
expect(result[0]?.valueScoreBreakdown).toMatchObject({ total: 95 });
});
it("returns empty array for roles not in the visible-roles list", async () => {
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue({
scoreVisibleRoles: ["ADMIN"],
}),
},
resource: {
findMany: vi.fn(),
},
};
const result = await getDashboardTopValueResources(db as never, {
limit: 10,
userRole: "VIEWER",
});
expect(result).toEqual([]);
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("falls back to default visible roles when settings are null", async () => {
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
// Default visible roles are ["ADMIN", "MANAGER"]
const adminResult = await getDashboardTopValueResources(db as never, {
limit: 5,
userRole: "ADMIN",
});
expect(adminResult).toEqual([]);
expect(db.resource.findMany).toHaveBeenCalledOnce();
const viewerResult = await getDashboardTopValueResources(db as never, {
limit: 5,
userRole: "VIEWER",
});
expect(viewerResult).toEqual([]);
// findMany should not have been called for VIEWER
expect(db.resource.findMany).toHaveBeenCalledOnce();
});
it("normalises a malformed valueScoreBreakdown to null", async () => {
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue({
scoreVisibleRoles: ["ADMIN"],
}),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_2",
eid: "E002",
displayName: "Bob",
chapter: null,
valueScore: 42,
valueScoreBreakdown: { corrupted: true }, // not a valid breakdown
valueScoreUpdatedAt: null,
lcrCents: 60_000,
country: null,
federalState: null,
metroCity: null,
},
]),
},
};
const result = await getDashboardTopValueResources(db as never, {
limit: 5,
userRole: "ADMIN",
});
expect(result[0]?.valueScoreBreakdown).toBeNull();
expect(result[0]?.countryCode).toBeNull();
expect(result[0]?.metroCityName).toBeNull();
});
});
// ---------------------------------------------------------------------------
// getDashboardProjectHealth
// ---------------------------------------------------------------------------
describe("getDashboardProjectHealth", () => {
it("computes budget, staffing and timeline health scores for an active project", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-10T00:00:00.000Z"));
try {
const db = {
project: {
findMany: vi.fn().mockResolvedValue([
{
id: "proj_1",
name: "Alpha",
shortCode: "ALPHA",
status: "ACTIVE",
budgetCents: 100_000,
endDate: new Date("2026-12-31T00:00:00.000Z"), // ON_TRACK
clientId: "client_1",
client: { name: "ACME" },
demandRequirements: [
{
id: "dr_1",
headcount: 2,
status: "CONFIRMED",
assignments: [{ id: "a1" }], // 1 of 2 filled
},
],
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
projectId: "proj_1",
startDate: new Date("2026-03-01T00:00:00.000Z"),
endDate: new Date("2026-03-03T00:00:00.000Z"),
dailyCostCents: 10_000,
resource: {
id: "res_1",
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
countryId: null,
federalState: null,
metroCityId: null,
country: null,
metroCity: null,
},
},
]),
},
};
const result = await getDashboardProjectHealth(db as never);
expect(result).toHaveLength(1);
const row = result[0]!;
expect(row.shortCode).toBe("ALPHA");
expect(row.clientName).toBe("ACME");
// budgetCents=100_000, spentCents=20_000 (2 working days Mon+Tue)
// pctUsed=20 → budgetHealth=80
expect(row.budgetHealth).toBe(80);
expect(row.spentCents).toBe(20_000);
expect(row.budgetUtilizationPercent).toBe(20);
// staffingHealth: 1 filled / 2 needed = 50
expect(row.staffingHealth).toBe(50);
// timelineHealth: end date is in the future → 100
expect(row.timelineHealth).toBe(100);
expect(row.timelineStatus).toBe("ON_TRACK");
// compositeScore = round((80+50+100)/3) = round(76.67) = 77
expect(row.compositeScore).toBe(77);
expect(row.demandHeadcountTotal).toBe(2);
expect(row.demandHeadcountFilled).toBe(1);
expect(row.demandHeadcountOpen).toBe(1);
} finally {
vi.useRealTimers();
}
});
it("returns empty array when no active projects exist", async () => {
const db = {
project: {
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
findMany: vi.fn(),
},
};
const result = await getDashboardProjectHealth(db as never);
expect(result).toEqual([]);
expect(db.assignment.findMany).not.toHaveBeenCalled();
});
it("marks a project OVERDUE and sets timelineHealth=0 when end date has passed", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-10T00:00:00.000Z"));
try {
const db = {
project: {
findMany: vi.fn().mockResolvedValue([
{
id: "proj_2",
name: "Beta",
shortCode: "BETA",
status: "ACTIVE",
budgetCents: null,
endDate: new Date("2026-01-01T00:00:00.000Z"), // in the past
clientId: null,
client: null,
demandRequirements: [],
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const result = await getDashboardProjectHealth(db as never);
expect(result).toHaveLength(1);
const row = result[0]!;
expect(row.timelineHealth).toBe(0);
expect(row.timelineStatus).toBe("OVERDUE");
// No budget defined → budgetHealth=100, no staffing demands → staffingHealth=100
// compositeScore = round((100+100+0)/3) = 67
expect(row.compositeScore).toBe(67);
} finally {
vi.useRealTimers();
}
});
it("sorts results by composite score ascending (worst-health project first)", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-10T00:00:00.000Z"));
try {
const db = {
project: {
findMany: vi.fn().mockResolvedValue([
{
id: "proj_good",
name: "Healthy",
shortCode: "GOOD",
status: "ACTIVE",
budgetCents: 100_000,
endDate: new Date("2026-12-31T00:00:00.000Z"),
clientId: null,
client: null,
demandRequirements: [],
},
{
id: "proj_bad",
name: "Overdue",
shortCode: "BAD",
status: "ACTIVE",
budgetCents: 1_000,
endDate: new Date("2025-01-01T00:00:00.000Z"), // overdue
clientId: null,
client: null,
demandRequirements: [
{
id: "dr_1",
headcount: 5,
status: "CONFIRMED",
assignments: [], // 0 filled
},
],
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
projectId: "proj_bad",
startDate: new Date("2026-03-01T00:00:00.000Z"),
endDate: new Date("2026-03-31T00:00:00.000Z"),
dailyCostCents: 100,
resource: {
id: "res_1",
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
countryId: null,
federalState: null,
metroCityId: null,
country: null,
metroCity: null,
},
},
]),
},
};
const result = await getDashboardProjectHealth(db as never);
expect(result).toHaveLength(2);
// "bad" project has lower composite score → sorted first
expect(result[0]?.shortCode).toBe("BAD");
expect(result[1]?.shortCode).toBe("GOOD");
expect(result[0]!.compositeScore).toBeLessThan(result[1]!.compositeScore);
} finally {
vi.useRealTimers();
}
});
});
// ---------------------------------------------------------------------------
// getDashboardPeakTimes resource-level grouping
// ---------------------------------------------------------------------------
describe("getDashboardPeakTimes (resource groupBy)", () => {
it("groups booked hours by individual resource name", async () => {
const db = {
assignment: {
findMany: vi.fn().mockResolvedValue([
{
id: "assign_1",
projectId: "proj_1",
resourceId: "res_1",
status: "ACTIVE",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-02T00:00:00.000Z"),
hoursPerDay: 6,
dailyCostCents: 0,
project: {
id: "proj_1",
name: "Alpha",
shortCode: "ALPHA",
status: "ACTIVE",
orderType: "FIXED",
},
resource: { id: "res_1", displayName: "Alice", chapter: "CGI" },
},
{
id: "assign_2",
projectId: "proj_1",
resourceId: "res_2",
status: "ACTIVE",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-02T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
project: {
id: "proj_1",
name: "Alpha",
shortCode: "ALPHA",
status: "ACTIVE",
orderType: "FIXED",
},
resource: { id: "res_2", displayName: "Bob", chapter: "Lighting" },
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
displayName: "Alice",
chapter: "CGI",
availability: { monday: 6, tuesday: 6, wednesday: 6, thursday: 6, friday: 6 },
countryId: null,
federalState: null,
metroCityId: null,
country: null,
metroCity: null,
},
{
id: "res_2",
displayName: "Bob",
chapter: "Lighting",
availability: { monday: 4, tuesday: 4, wednesday: 4, thursday: 4, friday: 4 },
countryId: null,
federalState: null,
metroCityId: null,
country: null,
metroCity: null,
},
]),
},
};
const result = await getDashboardPeakTimes(db as never, {
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-02T00:00:00.000Z"),
granularity: "month",
groupBy: "resource",
});
expect(result).toHaveLength(1);
const period = result[0]!;
expect(period.period).toBe("2026-03");
const alice = period.groups.find((g) => g.name === "Alice");
const bob = period.groups.find((g) => g.name === "Bob");
expect(alice).toBeDefined();
expect(alice?.hours).toBe(6);
expect(alice?.capacityHours).toBe(6);
expect(alice?.utilizationPct).toBe(100);
expect(bob).toBeDefined();
expect(bob?.hours).toBe(4);
expect(bob?.capacityHours).toBe(4);
});
it("returns empty periods list when date range yields no resources and no assignments", async () => {
const db = {
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const result = await getDashboardPeakTimes(db as never, {
startDate: new Date("2026-03-01T00:00:00.000Z"),
endDate: new Date("2026-03-01T00:00:00.000Z"),
granularity: "week",
groupBy: "project",
});
expect(result).toHaveLength(1);
const period = result[0]!;
expect(period.groups).toEqual([]);
expect(period.totalHours).toBe(0);
expect(period.capacityHours).toBe(0);
expect(period.utilizationPct).toBe(0);
});
});