test(api): cover assistant chargeability report

This commit is contained in:
2026-04-01 00:41:26 +02:00
parent 95940f005b
commit f52380dc53
@@ -0,0 +1,204 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionKey } from "@capakraken/shared";
import { listAssignmentBookings } from "@capakraken/application";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
approveEstimateVersion: vi.fn(),
cloneEstimate: vi.fn(),
commitDispoImportBatch: vi.fn(),
countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }),
createEstimateExport: vi.fn(),
createEstimatePlanningHandoff: vi.fn(),
createEstimateRevision: vi.fn(),
assessDispoImportReadiness: vi.fn(),
loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()),
getDashboardDemand: vi.fn().mockResolvedValue([]),
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardOverview: vi.fn(),
getDashboardSkillGapSummary: vi.fn().mockResolvedValue({
roleGaps: [],
totalOpenPositions: 0,
skillSupplyTop10: [],
resourcesByRole: [],
}),
getDashboardProjectHealth: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
getDashboardTopValueResources: vi.fn().mockResolvedValue([]),
getEstimateById: vi.fn(),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
stageDispoImportBatch: vi.fn(),
submitEstimateVersion: vi.fn(),
updateEstimateDraft: vi.fn(),
};
});
import { executeTool } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-report-test-helpers.js";
describe("assistant chargeability report tools", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns the chargeability report readmodel through the assistant", async () => {
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_confirmed",
projectId: "project_confirmed",
resourceId: "resource_1",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-06T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_confirmed",
name: "Confirmed Project",
shortCode: "CP",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
]);
const ctx = createToolContext(
{
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_1",
eid: "E-001",
displayName: "Alice",
fte: 1,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
countryId: "country_es",
federalState: null,
metroCityId: "city_1",
chargeabilityTarget: 80,
country: {
id: "country_es",
code: "ES",
dailyWorkingHours: 8,
scheduleRules: null,
},
orgUnit: { id: "org_1", name: "CGI" },
managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 },
managementLevel: { id: "level_1", name: "L7" },
metroCity: { id: "city_1", name: "Barcelona" },
},
]),
},
project: {
findMany: vi.fn().mockResolvedValue([
{ id: "project_confirmed", utilizationCategory: { code: "Chg" } },
]),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
},
{
permissions: [PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
},
);
const result = await executeTool(
"get_chargeability_report",
JSON.stringify({
startMonth: "2026-03",
endMonth: "2026-03",
resourceLimit: 10,
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
monthKeys: string[];
groupTotals: Array<{ monthKey: string; chargeabilityPct: number; targetPct: number }>;
resourceCount: number;
returnedResourceCount: number;
truncated: boolean;
explainability: {
locationFields: string[];
monthDerivationFields: string[];
activeFilters: string[];
formulas: {
sah: string;
chargeabilityPct: string;
targetHours: string;
gapHours: string;
};
notes: string[];
};
resources: Array<{
displayName: string;
targetPct: number;
months: Array<{ monthKey: string; sah: number; chargeabilityPct: number }>;
}>;
};
expect(parsed.monthKeys).toEqual(["2026-03"]);
expect(parsed.groupTotals).toEqual([
expect.objectContaining({
monthKey: "2026-03",
chargeabilityPct: expect.any(Number),
targetPct: 80,
}),
]);
expect(parsed.resourceCount).toBe(1);
expect(parsed.returnedResourceCount).toBe(1);
expect(parsed.truncated).toBe(false);
expect(parsed.explainability).toEqual({
locationFields: [
"country",
"federalState",
"city",
"orgUnit",
"managementLevelGroup",
"managementLevel",
],
monthDerivationFields: [
"baseAvailableHours",
"publicHolidayCount",
"publicHolidayWorkdayCount",
"publicHolidayHoursDeduction",
"absenceDayEquivalent",
"absenceHoursDeduction",
"effectiveAvailableHours",
],
activeFilters: [],
formulas: {
sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours",
chargeabilityPct: "chargeabilityHours / sahHours",
targetHours: "sahHours * targetPct",
gapHours: "chargeabilityHours - targetHours",
},
notes: [
"Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.",
"Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.",
"Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.",
],
});
expect(parsed.resources).toEqual([
expect.objectContaining({
displayName: "Alice",
targetPct: 80,
months: [
expect.objectContaining({
monthKey: "2026-03",
sah: expect.any(Number),
chargeabilityPct: expect.any(Number),
}),
],
}),
]);
});
});