Files
CapaKraken/packages/api/src/__tests__/assistant-tools-dashboard-detail.test.ts
T

465 lines
14 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
import {
createToolContext,
executeTool,
getDashboardChargeabilityOverview,
getDashboardDemand,
getDashboardOverview,
getDashboardPeakTimes,
getDashboardProjectHealth,
getDashboardSkillGapSummary,
getDashboardTopValueResources,
} from "./assistant-tools-dashboard-test-helpers.js";
describe("assistant dashboard tools detail aggregation", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("routes dashboard detail reads through dashboard router callers", async () => {
vi.mocked(getDashboardOverview).mockResolvedValue({
totalResources: 12,
activeResources: 10,
inactiveResources: 2,
totalProjects: 4,
activeProjects: 3,
inactiveProjects: 1,
totalAllocations: 8,
activeAllocations: 7,
cancelledAllocations: 1,
approvedVacations: 2,
totalEstimates: 5,
budgetSummary: {
totalBudgetCents: 500_000,
totalCostCents: 240_000,
avgUtilizationPercent: 48,
},
budgetBasis: {
remainingBudgetCents: 260_000,
budgetedProjects: 3,
unbudgetedProjects: 1,
trackedAssignmentCount: 8,
windowStart: new Date("2026-01-01T00:00:00.000Z"),
windowEnd: new Date("2026-06-30T00:00:00.000Z"),
},
recentActivity: [],
projectsByStatus: [],
chapterUtilization: [
{
chapter: "Delivery",
resourceCount: 4,
avgChargeabilityTarget: 78,
},
],
});
vi.mocked(getDashboardPeakTimes).mockResolvedValue([
{
period: "2026-03",
groups: [],
totalHours: 320.4,
capacityHours: 400.2,
utilizationPct: 80,
derivation: {
periodStart: "2026-03-01",
periodEnd: "2026-03-31",
calendarContextCount: 1,
resourceCount: 4,
groupCount: 1,
baseAvailableHours: 416.2,
calendarLocations: [
{
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Augsburg",
resourceCount: 4,
effectiveAvailableHours: 400.2,
},
],
bookedHours: 320.4,
capacityHours: 400.2,
remainingCapacityHours: 79.8,
overbookedHours: 0,
utilizationPct: 80,
effectiveAvailableHours: 400.2,
publicHolidayHoursDeduction: 16,
absenceDayEquivalent: 1.5,
absenceHoursDeduction: 12.5,
},
},
]);
vi.mocked(getDashboardTopValueResources).mockResolvedValue([
{
id: "res_1",
eid: "pparker",
displayName: "Peter Parker",
chapter: "Delivery",
valueScore: 91,
valueScoreBreakdown: {
skillDepth: 85,
skillBreadth: 74,
costEfficiency: 93,
chargeability: 78,
experience: 88,
total: 91,
},
valueScoreUpdatedAt: new Date("2026-03-03T00:00:00.000Z"),
lcrCents: 9_500,
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Augsburg",
},
]);
vi.mocked(getDashboardDemand).mockResolvedValue([
{
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
allocatedHours: 120,
requiredFTEs: 4,
resourceCount: 2,
derivation: {
periodStart: "2026-01-01",
periodEnd: "2026-06-30",
periodWorkingHoursBase: 1040,
requiredHours: 2080,
requiredFTEs: 4,
fillPct: 50,
demandSource: "DEMAND_REQUIREMENTS",
calendarLocations: [
{
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Augsburg",
resourceCount: 2,
allocatedHours: 120,
},
],
},
},
]);
vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({
rows: [
{
id: "res_1",
eid: "pparker",
displayName: "Peter Parker",
chapter: "Delivery",
chargeabilityTarget: 78,
actualChargeability: 70,
expectedChargeability: 76,
derivation: {
weeklyAvailabilityHours: 40,
baseWorkingDays: 22,
effectiveWorkingDayEquivalent: 21,
baseAvailableHours: 176,
effectiveAvailableHours: 168,
publicHolidayCount: 1,
publicHolidayWorkdayCount: 1,
publicHolidayHoursDeduction: 8,
absenceDayEquivalent: 0,
absenceHoursDeduction: 0,
actualBookedHours: 117.6,
expectedBookedHours: 127.7,
targetBookedHours: 131,
unassignedHours: 40.3,
},
},
],
top: [],
watchlist: [],
month: "2026-03",
});
vi.mocked(getDashboardProjectHealth).mockResolvedValue([
{
id: "project_1",
projectName: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
clientId: "client_1",
clientName: "Acme",
budgetHealth: 58,
staffingHealth: 46,
timelineHealth: 61,
compositeScore: 55,
budgetUtilizationPercent: 73,
remainingBudgetCents: 88_000,
demandHeadcountTotal: 4,
demandHeadcountFilled: 2,
demandHeadcountOpen: 2,
demandRequirementCount: 2,
plannedEndDate: new Date("2026-09-30T00:00:00.000Z"),
daysUntilEndDate: 183,
timelineStatus: "DUE_SOON",
derivation: {
periodStart: "2026-01-01",
periodEnd: "2026-06-30",
calendarContextCount: 1,
holidayAwareAssignmentCount: 2,
fallbackAssignmentCount: 0,
baseSpentCents: 140_000,
adjustedSpentCents: 132_000,
publicHolidayDayEquivalent: 1,
publicHolidayCostDeductionCents: 5_000,
absenceDayEquivalent: 0.5,
absenceCostDeductionCents: 3_000,
},
},
]);
vi.mocked(getDashboardSkillGapSummary).mockResolvedValue({
roleGaps: [
{ role: "Pipeline TD", needed: 4, filled: 1, gap: 3, fillRate: 25 },
{ role: "Lighting TD", needed: 2, filled: 1, gap: 1, fillRate: 50 },
],
totalOpenPositions: 4,
skillSupplyTop10: [
{ skill: "houdini", resourceCount: 5 },
{ skill: "nuke", resourceCount: 4 },
],
resourcesByRole: [
{ role: "Compositor", count: 6 },
{ role: "Pipeline TD", count: 2 },
],
});
const ctx = createToolContext(
{
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
{ userRole: SystemRole.CONTROLLER },
);
const result = await executeTool("get_dashboard_detail", JSON.stringify({ section: "all" }), ctx);
expect(getDashboardOverview).toHaveBeenCalledTimes(1);
expect(getDashboardPeakTimes).toHaveBeenCalledWith(
ctx.db,
expect.objectContaining({
startDate: new Date("2026-01-01T00:00:00.000Z"),
endDate: new Date("2026-06-30T00:00:00.000Z"),
granularity: "month",
groupBy: "project",
}),
);
expect(getDashboardTopValueResources).toHaveBeenCalledWith(
ctx.db,
expect.objectContaining({
limit: 10,
userRole: SystemRole.CONTROLLER,
}),
);
expect(getDashboardDemand).toHaveBeenCalledWith(
ctx.db,
expect.objectContaining({
startDate: new Date("2026-01-01T00:00:00.000Z"),
endDate: new Date("2026-06-30T00:00:00.000Z"),
groupBy: "project",
}),
);
expect(JSON.parse(result.content)).toEqual({
peakTimes: [
{
month: "2026-03",
totalHours: 320.4,
totalHoursPerDay: 320.4,
capacityHours: 400.2,
utilizationPct: 80,
calendarContextCount: 1,
calendarLocations: [
{
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Augsburg",
resourceCount: 4,
effectiveAvailableHours: 400.2,
},
],
explainability: {
periodStart: "2026-03-01",
periodEnd: "2026-03-31",
resourceCount: 4,
groupCount: 1,
baseAvailableHours: 416.2,
effectiveAvailableHours: 400.2,
publicHolidayHoursDeduction: 16,
absenceDayEquivalent: 1.5,
absenceHoursDeduction: 12.5,
remainingCapacityHours: 79.8,
overbookedHours: 0,
},
},
],
topResources: [
{
name: "Peter Parker",
eid: "pparker",
chapter: "Delivery",
lcr: "95,00 EUR",
valueScore: 91,
valueScoreBreakdown: {
skillDepth: 85,
skillBreadth: 74,
costEfficiency: 93,
chargeability: 78,
experience: 88,
total: 91,
},
valueScoreUpdatedAt: "2026-03-03T00:00:00.000Z",
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Augsburg",
},
],
demandPipeline: [
{
project: "Gelddruckmaschine (GDM)",
needed: 2,
requiredFTEs: 4,
allocatedResources: 2,
allocatedHours: 120,
calendarLocations: [
{
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Augsburg",
resourceCount: 2,
allocatedHours: 120,
},
],
explainability: {
periodStart: "2026-01-01",
periodEnd: "2026-06-30",
periodWorkingHoursBase: 1040,
requiredHours: 2080,
fillPct: 50,
demandSource: "DEMAND_REQUIREMENTS",
calendarContextCount: 1,
},
},
],
chargeabilityByChapter: [
{
chapter: "Delivery",
headcount: 1,
avgTargetPct: 78,
avgActualPct: 70,
avgExpectedPct: 76,
gapToTargetPct: 8,
avgTarget: "78%",
avgActual: "70%",
avgExpected: "76%",
explainability: {
month: "2026-03",
resourceCount: 1,
derivedHeadcount: 1,
baseAvailableHours: 176,
effectiveAvailableHours: 168,
actualBookedHours: 117.6,
expectedBookedHours: 127.7,
targetBookedHours: 131,
publicHolidayHoursDeduction: 8,
absenceDayEquivalent: 0,
absenceHoursDeduction: 0,
unassignedHours: 40.3,
},
},
],
projectHealth: [
{
project: "Gelddruckmaschine (GDM)",
status: "ACTIVE",
overall: 55,
rating: "at_risk",
budget: 58,
staffing: 46,
timeline: 61,
timelineStatus: "DUE_SOON",
daysUntilEndDate: 183,
demandHeadcountOpen: 2,
explainability: {
demandHeadcountTotal: 4,
demandHeadcountFilled: 2,
demandHeadcountOpen: 2,
demandRequirementCount: 2,
plannedEndDate: "2026-09-30T00:00:00.000Z",
budgetUtilizationPercent: 73,
remainingBudgetCents: 88_000,
calendarContextCount: 1,
holidayAwareAssignmentCount: 2,
publicHolidayCostDeductionCents: 5_000,
absenceCostDeductionCents: 3_000,
},
},
],
skillGaps: {
totalOpenPositions: 4,
roleGaps: [
{ role: "Pipeline TD", gap: 3, needed: 4, filled: 1, fillRate: 25 },
{ role: "Lighting TD", gap: 1, needed: 2, filled: 1, fillRate: 50 },
],
topSkillsInSupply: [
{ skill: "houdini", resourceCount: 5 },
{ skill: "nuke", resourceCount: 4 },
],
resourcesByRole: [
{ role: "Compositor", count: 6 },
{ role: "Pipeline TD", count: 2 },
],
},
});
});
it("routes the isolated skill gap detail section without unrelated dashboard reads", async () => {
vi.mocked(getDashboardSkillGapSummary).mockResolvedValue({
roleGaps: [
{ role: "Pipeline TD", needed: 4, filled: 1, gap: 3, fillRate: 25 },
],
totalOpenPositions: 3,
skillSupplyTop10: [{ skill: "houdini", resourceCount: 5 }],
resourcesByRole: [{ role: "Pipeline TD", count: 2 }],
});
const ctx = createToolContext(
{
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
{ userRole: SystemRole.CONTROLLER },
);
const result = await executeTool(
"get_dashboard_detail",
JSON.stringify({ section: "skill_gaps" }),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
skillGaps: {
totalOpenPositions: 3,
roleGaps: [
{ role: "Pipeline TD", gap: 3, needed: 4, filled: 1, fillRate: 25 },
],
topSkillsInSupply: [{ skill: "houdini", resourceCount: 5 }],
resourcesByRole: [{ role: "Pipeline TD", count: 2 }],
},
});
expect(getDashboardSkillGapSummary).toHaveBeenCalledTimes(1);
expect(getDashboardOverview).not.toHaveBeenCalled();
expect(getDashboardPeakTimes).not.toHaveBeenCalled();
expect(getDashboardDemand).not.toHaveBeenCalled();
expect(getDashboardProjectHealth).not.toHaveBeenCalled();
expect(getDashboardChargeabilityOverview).not.toHaveBeenCalled();
expect(getDashboardTopValueResources).not.toHaveBeenCalled();
});
});