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(); 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(); 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.", ], }, }); }); });