test(api): cover assistant report reads

This commit is contained in:
2026-03-31 23:56:15 +02:00
parent c03436945e
commit 8c9e43512f
2 changed files with 322 additions and 0 deletions
@@ -0,0 +1,293 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
import { listAssignmentBookings } from "@capakraken/application";
import { loadResourceDailyAvailabilityContexts } from "../lib/resource-capacity.js";
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(),
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(),
};
});
vi.mock("../lib/resource-capacity.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../lib/resource-capacity.js")>();
return {
...actual,
calculateEffectiveAvailableHours: vi.fn(({ context }: { context?: unknown }) => (context ? 160 : 176)),
calculateEffectiveBookedHours: vi.fn(() => 0),
countEffectiveWorkingDays: vi.fn(({ context }: { context?: unknown }) => (context ? 20 : 22)),
getAvailabilityHoursForDate: vi.fn(() => 8),
loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()),
};
});
import { executeTool } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-report-test-helpers.js";
describe("assistant report read tools", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("routes report reads through the report router path", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
displayName: "Bruce Banner",
chapter: "Delivery",
country: { name: "Germany" },
},
]),
count: vi.fn().mockResolvedValue(1),
},
};
const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER });
const result = await executeTool(
"run_report",
JSON.stringify({
entity: "resource",
columns: ["displayName", "chapter", "country.name"],
filters: [{ field: "displayName", op: "contains", value: "Bruce" }],
limit: 25,
}),
ctx,
);
expect(db.resource.findMany).toHaveBeenCalledWith({
select: {
id: true,
displayName: true,
chapter: true,
country: { select: { name: true } },
},
where: {
displayName: { contains: "Bruce", mode: "insensitive" },
},
take: 25,
skip: 0,
});
expect(db.resource.count).toHaveBeenCalledWith({
where: {
displayName: { contains: "Bruce", mode: "insensitive" },
},
});
expect(JSON.parse(result.content)).toEqual({
rows: [
{
id: "res_1",
displayName: "Bruce Banner",
chapter: "Delivery",
"country.name": "Germany",
},
],
rowCount: 1,
totalCount: 1,
columns: ["id", "displayName", "chapter", "country.name"],
groups: [],
});
});
it("passes report grouping and sorting options through the assistant tool", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
displayName: "Bruce Banner",
chapter: "Delivery",
},
]),
count: vi.fn().mockResolvedValue(1),
},
};
const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER });
await executeTool(
"run_report",
JSON.stringify({
entity: "resource",
columns: ["displayName", "chapter"],
groupBy: "chapter",
sortBy: "displayName",
sortDir: "desc",
limit: 25,
}),
ctx,
);
expect(db.resource.findMany).toHaveBeenCalledWith({
select: {
id: true,
displayName: true,
chapter: true,
},
where: {},
orderBy: [{ chapter: "asc" }, { displayName: "desc" }],
take: 25,
skip: 0,
});
});
it("adds explainability metadata for resource_month reports", async () => {
vi.mocked(listAssignmentBookings).mockResolvedValue([]);
vi.mocked(loadResourceDailyAvailabilityContexts).mockResolvedValue(new Map([
[
"res_1",
{
holidayDates: new Set(["2026-01-06"]),
absenceFractionsByDate: new Map([["2026-01-12", 1]]),
vacationFractionsByDate: new Map([["2026-01-12", 1]]),
},
],
]));
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
eid: "E-1",
displayName: "Peter Parker",
email: "peter@example.com",
chapter: "Delivery",
resourceType: "EMPLOYEE",
isActive: true,
chgResponsibility: false,
rolledOff: false,
departed: false,
lcrCents: 10000,
ucrCents: 15000,
currency: "EUR",
fte: 1,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
chargeabilityTarget: 80,
federalState: "Bayern",
countryId: "country_de",
metroCityId: "metro_muc",
country: { code: "DE", name: "Germany" },
metroCity: { name: "Munich" },
orgUnit: { name: "Consulting" },
managementLevelGroup: { name: "Senior", targetPercentage: 0.8 },
managementLevel: { name: "Consultant" },
},
]),
},
};
const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER });
const result = await executeTool(
"run_report",
JSON.stringify({
entity: "resource_month",
periodMonth: "2026-01",
columns: [
"displayName",
"countryCode",
"federalState",
"metroCityName",
"monthlyBaseAvailableHours",
"monthlyPublicHolidayCount",
"monthlyPublicHolidayHoursDeduction",
"monthlyAbsenceDayEquivalent",
"monthlyAbsenceHoursDeduction",
"monthlySahHours",
],
limit: 25,
}),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
rows: [
{
id: "res_1:2026-01",
displayName: "Peter Parker",
countryCode: "DE",
federalState: "Bayern",
metroCityName: "Munich",
monthlyBaseAvailableHours: 176,
monthlyPublicHolidayCount: 1,
monthlyPublicHolidayHoursDeduction: 8,
monthlyAbsenceDayEquivalent: 1,
monthlyAbsenceHoursDeduction: 8,
monthlySahHours: 160,
},
],
rowCount: 1,
totalCount: 1,
columns: [
"id",
"displayName",
"countryCode",
"federalState",
"metroCityName",
"monthlyBaseAvailableHours",
"monthlyPublicHolidayCount",
"monthlyPublicHolidayHoursDeduction",
"monthlyAbsenceDayEquivalent",
"monthlyAbsenceHoursDeduction",
"monthlySahHours",
],
groups: [],
explainability: {
entity: "resource_month",
periodMonth: "2026-01",
locationContextColumns: ["countryCode", "federalState", "metroCityName"],
holidayMetricColumns: ["monthlyPublicHolidayCount", "monthlyPublicHolidayHoursDeduction"],
absenceMetricColumns: ["monthlyAbsenceDayEquivalent", "monthlyAbsenceHoursDeduction"],
capacityMetricColumns: ["monthlyBaseAvailableHours", "monthlySahHours"],
chargeabilityMetricColumns: [],
missingRecommendedColumns: [
"countryName",
"monthlyPublicHolidayWorkdayCount",
"monthlyBaseWorkingDays",
"monthlyEffectiveWorkingDays",
"monthlyChargeabilityTargetPct",
"monthlyTargetHours",
],
notes: [
"monthlySahHours already reflects region-specific public holidays from country, federal state, and city context when those attributes exist on the resource.",
"monthlyAbsence* metrics only deduct workdays that are not already counted as public holidays.",
"monthlyBaseAvailableHours shows pre-deduction capacity; compare it with holiday, absence, and SAH columns to explain the final monthly availability.",
],
},
});
});
});