From 7e85be8f76d9755b51fc1cfd152d29e89bbfdfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:02:58 +0200 Subject: [PATCH] test(api): cover assistant vacation entitlements --- ...-tools-vacation-entitlement-errors.test.ts | 110 ++++++++++++++ ...ant-tools-vacation-entitlement-set.test.ts | 89 +++++++++++ ...tools-vacation-entitlement-summary.test.ts | 139 ++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-vacation-entitlement-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-vacation-entitlement-set.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-vacation-entitlement-summary.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-vacation-entitlement-errors.test.ts b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-errors.test.ts new file mode 100644 index 0000000..b5cf640 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-errors.test.ts @@ -0,0 +1,110 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +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 entitlement error paths", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable assistant error when the entitlement resource disappears before saving", async () => { + const ctx = createToolContext( + { + resource: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "res_1", + eid: "EMP-001", + displayName: "Alice Example", + chapter: "Delivery", + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + vacationEntitlement: { + findUnique: vi.fn().mockResolvedValue(null), + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "VacationEntitlement_resourceId_fkey" }, + }), + }, + }, + { userRole: SystemRole.MANAGER }, + ); + + const result = await executeTool( + "set_entitlement", + JSON.stringify({ resourceId: "EMP-001", year: 2027, entitledDays: 32 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Resource not found with the given criteria.", + }); + }); + + it("returns a stable assistant error when entitlement carryover is passed manually", async () => { + const ctx = createToolContext( + { + resource: { + findUnique: vi.fn(), + findFirst: vi.fn(), + }, + }, + { userRole: SystemRole.MANAGER }, + ); + + const result = await executeTool( + "set_entitlement", + JSON.stringify({ + resourceId: "EMP-001", + year: 2027, + entitledDays: 32, + carryoverDays: 3, + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.", + }); + expect(ctx.db.resource.findUnique).not.toHaveBeenCalled(); + expect(ctx.db.resource.findFirst).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-vacation-entitlement-set.test.ts b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-set.test.ts new file mode 100644 index 0000000..edc7d43 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-set.test.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +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 entitlement mutation tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("sets entitlement through the real entitlement workflow", async () => { + const db = { + resource: { + findUnique: vi.fn().mockResolvedValue({ + id: "res_1", + eid: "EMP-001", + displayName: "Alice Example", + chapter: "Delivery", + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + vacationEntitlement: { + findUnique: vi.fn().mockResolvedValue(null), + create: vi.fn().mockImplementation(async (args?: any) => ({ + id: `ent_${args?.data?.year ?? "unknown"}`, + resourceId: args?.data?.resourceId ?? "res_1", + year: args?.data?.year ?? 2027, + entitledDays: args?.data?.entitledDays ?? 32, + carryoverDays: 0, + usedDays: 0, + pendingDays: 0, + })), + }, + }; + const ctx = createToolContext(db, { userRole: SystemRole.ADMIN }); + + const result = await executeTool( + "set_entitlement", + JSON.stringify({ resourceId: "res_1", year: 2027, entitledDays: 32 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual(expect.objectContaining({ + success: true, + message: "Set entitlement for Alice Example (2027): 32 days", + })); + expect(db.vacationEntitlement.create).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.objectContaining({ + resourceId: "res_1", + year: 2027, + entitledDays: 32, + }), + })); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-vacation-entitlement-summary.test.ts b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-summary.test.ts new file mode 100644 index 0000000..3f11213 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-vacation-entitlement-summary.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +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 entitlement summary tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes entitlement summary through the entitlement year summary workflow", async () => { + const db = { + systemSettings: { + findUnique: vi.fn().mockResolvedValue({ vacationDefaultDays: 28 }), + }, + resource: { + findMany: vi.fn().mockResolvedValue([ + { + id: "res_1", + displayName: "Alice Example", + eid: "EMP-001", + lcrCents: 0, + chapter: "Delivery", + federalState: "BY", + country: { code: "DE", name: "Germany" }, + metroCity: { name: "Muenchen" }, + }, + { + id: "res_2", + displayName: "Bob Example", + eid: "EMP-002", + lcrCents: 0, + chapter: "CGI", + federalState: "HH", + country: { code: "DE", name: "Germany" }, + metroCity: { name: "Hamburg" }, + }, + ]), + }, + vacationEntitlement: { + findUnique: vi.fn().mockResolvedValue(null), + create: vi.fn().mockImplementation(async (args?: any) => ({ + id: `ent_${args?.data?.resourceId ?? "unknown"}_${args?.data?.year ?? "unknown"}`, + resourceId: args?.data?.resourceId ?? "res_1", + year: args?.data?.year ?? 2026, + entitledDays: args?.data?.entitledDays ?? 28, + carryoverDays: args?.data?.carryoverDays ?? 0, + usedDays: args?.data?.usedDays ?? 0, + pendingDays: args?.data?.pendingDays ?? 0, + })), + update: vi.fn().mockImplementation(async (args?: any) => ({ + id: args?.where?.id ?? "ent_unknown", + resourceId: args?.where?.id?.includes("res_2") ? "res_2" : "res_1", + year: 2026, + entitledDays: 28, + carryoverDays: 0, + usedDays: args?.data?.usedDays ?? 0, + pendingDays: args?.data?.pendingDays ?? 0, + })), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + }; + const ctx = createToolContext(db, { userRole: SystemRole.ADMIN }); + + const result = await executeTool( + "get_entitlement_summary", + JSON.stringify({ year: 2026, resourceName: "alice" }), + ctx, + ); + + expect(db.resource.findMany).toHaveBeenCalledWith({ + where: { isActive: true }, + select: { + id: true, + displayName: true, + eid: true, + lcrCents: true, + chapter: true, + federalState: true, + country: { select: { code: true, name: true } }, + metroCity: { select: { name: true } }, + }, + orderBy: [{ chapter: "asc" }, { displayName: "asc" }], + }); + expect(JSON.parse(result.content)).toEqual([ + { + resource: "Alice Example", + eid: "EMP-001", + chapter: "Delivery", + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: "Muenchen", + year: 2026, + entitled: 28, + carryover: 0, + used: 0, + pending: 0, + remaining: 28, + }, + ]); + }); +});