From c03436945ef09fbf5f9c067983d62ea7dec45c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 23:55:28 +0200 Subject: [PATCH] test(api): cover assistant vacation read tools --- .../assistant-tools-vacation-balance.test.ts | 214 ++++++++++++++++++ ...tools-vacation-entitlement-test-helpers.ts | 29 +++ ...stant-tools-vacation-upcoming-read.test.ts | 141 ++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-vacation-balance.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-vacation-entitlement-test-helpers.ts create mode 100644 packages/api/src/__tests__/assistant-tools-vacation-upcoming-read.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-vacation-balance.test.ts b/packages/api/src/__tests__/assistant-tools-vacation-balance.test.ts new file mode 100644 index 0000000..7f58f67 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-vacation-balance.test.ts @@ -0,0 +1,214 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { VacationType } from "@capakraken/db"; +import { SystemRole } from "@capakraken/shared"; + +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(), + 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-vacation-entitlement-test-helpers.js"; + +describe("assistant vacation balance tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes vacation balance through the real vacation workflow", async () => { + const vacationFindMany = vi.fn().mockImplementation(async (args?: any) => { + if (args?.where?.type === VacationType.PUBLIC_HOLIDAY) { + return []; + } + if (args?.where?.type?.in) { + return [ + { + type: VacationType.ANNUAL, + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-06T00:00:00.000Z"), + status: "APPROVED", + isHalfDay: false, + }, + { + type: VacationType.ANNUAL, + startDate: new Date("2026-02-03T00:00:00.000Z"), + endDate: new Date("2026-02-03T00:00:00.000Z"), + status: "PENDING", + isHalfDay: true, + }, + ]; + } + if (args?.where?.type === VacationType.SICK) { + return [ + { + startDate: new Date("2026-03-10T00:00:00.000Z"), + endDate: new Date("2026-03-10T00:00:00.000Z"), + isHalfDay: false, + }, + ]; + } + return []; + }); + const ctx = createToolContext({ + resource: { + findUnique: vi.fn().mockResolvedValue({ + id: "res_1", + eid: "EMP-001", + displayName: "Alice Example", + userId: "user_1", + chapter: "Delivery", + federalState: "BY", + countryId: "country_de", + metroCityId: null, + country: { code: "DE", name: "Germany" }, + metroCity: null, + }), + }, + holidayCalendar: { + findMany: vi.fn().mockResolvedValue([]), + }, + systemSettings: { + findUnique: vi.fn().mockResolvedValue({ vacationDefaultDays: 30 }), + }, + vacationEntitlement: { + findUnique: vi.fn().mockResolvedValue(null), + create: vi.fn().mockResolvedValue({ + id: "ent_2026", + resourceId: "res_1", + year: 2026, + entitledDays: 30, + carryoverDays: 0, + usedDays: 0, + pendingDays: 0, + }), + update: vi.fn().mockImplementation(async (args?: any) => ({ + id: "ent_2026", + resourceId: "res_1", + year: 2026, + entitledDays: 30, + carryoverDays: 0, + usedDays: args?.data?.usedDays ?? 0, + pendingDays: args?.data?.pendingDays ?? 0, + })), + }, + vacation: { + findMany: vacationFindMany, + }, + }, { userRole: SystemRole.ADMIN }); + + const result = await executeTool( + "get_vacation_balance", + JSON.stringify({ resourceId: "res_1", year: 2026 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual(expect.objectContaining({ + resource: "Alice Example", + eid: "EMP-001", + year: 2026, + entitlement: 30, + carryOver: 0, + taken: 1, + pending: 0.5, + remaining: 28.5, + sickDays: 1, + deductionSummary: { + formula: "remaining = entitlement - taken - pending", + approvedVacationCount: 1, + pendingVacationCount: 1, + approvedRequestedDays: 2, + pendingRequestedDays: 0.5, + approvedDeductedDays: 1, + pendingDeductedDays: 0.5, + excludedHolidayDates: ["2026-01-06"], + holidayBasisVariants: ["Germany / BY"], + sources: { + hasCalendarHolidays: true, + hasLegacyPublicHolidayEntries: false, + }, + }, + vacations: [ + expect.objectContaining({ + type: "ANNUAL", + status: "APPROVED", + startDate: "2026-01-05", + endDate: "2026-01-06", + isHalfDay: false, + requestedDays: 2, + deductedDays: 1, + holidayCountryCode: "DE", + holidayCountryName: "Germany", + holidayFederalState: "BY", + holidayMetroCityName: null, + holidayCalendarDates: ["2026-01-06"], + holidayLegacyPublicHolidayDates: [], + holidayDetails: [{ date: "2026-01-06", source: "CALENDAR" }], + holidayContext: { + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: null, + sources: { + hasCalendarHolidays: true, + hasLegacyPublicHolidayEntries: false, + }, + }, + }), + expect.objectContaining({ + type: "ANNUAL", + status: "PENDING", + startDate: "2026-02-03", + endDate: "2026-02-03", + isHalfDay: true, + requestedDays: 0.5, + deductedDays: 0.5, + holidayCountryCode: "DE", + holidayCountryName: "Germany", + holidayFederalState: "BY", + holidayMetroCityName: null, + holidayCalendarDates: [], + holidayLegacyPublicHolidayDates: [], + holidayDetails: [], + holidayContext: { + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: null, + sources: { + hasCalendarHolidays: false, + hasLegacyPublicHolidayEntries: false, + }, + }, + }), + ], + })); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-vacation-entitlement-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-test-helpers.ts new file mode 100644 index 0000000..dceafe0 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-test-helpers.ts @@ -0,0 +1,29 @@ +import { PermissionKey, SystemRole } from "@capakraken/shared"; + +import type { ToolContext } from "../router/assistant-tools.js"; + +export function createToolContext( + db: Record, + options?: { + permissions?: PermissionKey[]; + userRole?: SystemRole; + }, +): ToolContext { + const userRole = options?.userRole ?? SystemRole.ADMIN; + return { + db: db as ToolContext["db"], + userId: "user_1", + userRole, + permissions: new Set(options?.permissions ?? []), + session: { + user: { email: "assistant@example.com", name: "Assistant User", image: null }, + expires: "2026-03-29T00:00:00.000Z", + }, + dbUser: { + id: "user_1", + systemRole: userRole, + permissionOverrides: null, + }, + roleDefaults: null, + }; +} diff --git a/packages/api/src/__tests__/assistant-tools-vacation-upcoming-read.test.ts b/packages/api/src/__tests__/assistant-tools-vacation-upcoming-read.test.ts new file mode 100644 index 0000000..218482f --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-vacation-upcoming-read.test.ts @@ -0,0 +1,141 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { VacationType } from "@capakraken/db"; +import { SystemRole } from "@capakraken/shared"; + +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(), + 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-planning-read-test-helpers.js"; + +describe("assistant planning allocation and vacation read tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes upcoming vacation reads through the vacation router path and post-filters by chapter", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-01T00:00:00.000Z")); + + try { + const db = { + vacation: { + findMany: vi.fn().mockResolvedValue([ + { + id: "vac_1", + resourceId: "res_1", + status: "APPROVED", + type: VacationType.ANNUAL, + startDate: new Date("2026-03-05T00:00:00.000Z"), + endDate: new Date("2026-03-06T00:00:00.000Z"), + isHalfDay: false, + halfDayPart: null, + resource: { + id: "res_1", + displayName: "Bruce Banner", + eid: "EMP-001", + lcrCents: 8_000, + chapter: "Delivery", + }, + requestedBy: null, + approvedBy: null, + }, + { + id: "vac_2", + resourceId: "res_2", + status: "APPROVED", + type: VacationType.ANNUAL, + startDate: new Date("2026-03-07T00:00:00.000Z"), + endDate: new Date("2026-03-07T00:00:00.000Z"), + isHalfDay: true, + halfDayPart: "MORNING", + resource: { + id: "res_2", + displayName: "Tony Stark", + eid: "EMP-002", + lcrCents: 9_000, + chapter: "Finance", + }, + requestedBy: null, + approvedBy: null, + }, + ]), + }, + }; + const ctx = createToolContext(db, { userRole: SystemRole.MANAGER }); + + const result = await executeTool( + "list_vacations_upcoming", + JSON.stringify({ chapter: "Delivery", daysAhead: 14, limit: 10 }), + ctx, + ); + + expect(db.vacation.findMany).toHaveBeenCalledWith({ + where: { + status: "APPROVED", + endDate: { gte: new Date("2026-03-01T00:00:00.000Z") }, + startDate: { lte: new Date("2026-03-15T00:00:00.000Z") }, + }, + include: { + resource: { + select: { + id: true, + displayName: true, + eid: true, + lcrCents: true, + chapter: true, + }, + }, + requestedBy: { select: { id: true, name: true, email: true } }, + approvedBy: { select: { id: true, name: true, email: true } }, + }, + orderBy: { startDate: "asc" }, + take: 10, + }); + expect(JSON.parse(result.content)).toEqual([ + { + resource: "Bruce Banner", + eid: "EMP-001", + chapter: "Delivery", + type: VacationType.ANNUAL, + start: "2026-03-05", + end: "2026-03-06", + isHalfDay: false, + halfDayPart: null, + }, + ]); + } finally { + vi.useRealTimers(); + } + }); +});