From 6a95a0105bb1ae735e31579b6b5603b69478a049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 23:52:59 +0200 Subject: [PATCH] test(api): cover holiday-aware budget and shoring tools --- ...stant-tools-holiday-budget-shoring.test.ts | 160 ++++++++++++++++++ ...ant-tools-holiday-capacity-test-helpers.ts | 26 +++ 2 files changed, 186 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-budget-shoring.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-capacity-test-helpers.ts diff --git a/packages/api/src/__tests__/assistant-tools-holiday-budget-shoring.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-budget-shoring.test.ts new file mode 100644 index 0000000..9380dda --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-budget-shoring.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { getDashboardBudgetForecast } from "@capakraken/application"; +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-holiday-capacity-test-helpers.js"; + +describe("assistant holiday-aware budget and shoring tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getDashboardBudgetForecast).mockResolvedValue([]); + }); + + it("returns holiday-aware budget forecast data from the dashboard use-case", async () => { + vi.mocked(getDashboardBudgetForecast).mockResolvedValue([ + { + projectId: "project_1", + projectName: "Gelddruckmaschine", + shortCode: "GDM", + budgetCents: 100_000, + spentCents: 60_000, + burnRate: 5_000, + pctUsed: 60, + estimatedExhaustionDate: "2026-02-20", + derivation: { + periodStart: "2026-02-01", + periodEnd: "2026-02-28", + calendarContextCount: 1, + holidayAwareAssignmentCount: 1, + fallbackAssignmentCount: 0, + baseBurnRateCents: 6_000, + adjustedBurnRateCents: 5_000, + publicHolidayDayEquivalent: 1, + publicHolidayCostDeductionCents: 1_000, + absenceDayEquivalent: 0, + absenceCostDeductionCents: 0, + }, + }, + ]); + + const ctx = createToolContext({}, ["viewCosts"]); + const result = await executeTool("get_budget_forecast", "{}", ctx); + const parsed = JSON.parse(result.content) as { + forecasts: Array<{ + projectName: string; + shortCode: string; + budgetCents: number; + spentCents: number; + remainingCents: number; + projectedCents: number; + burnRateCents: number; + burnStatus: string; + derivation: { + baseBurnRateCents: number; + adjustedBurnRateCents: number; + publicHolidayCostDeductionCents: number; + } | null; + }>; + }; + + expect(parsed.forecasts).toEqual([ + expect.objectContaining({ + projectName: "Gelddruckmaschine", + shortCode: "GDM", + budgetCents: 100_000, + spentCents: 60_000, + remainingCents: 40_000, + projectedCents: 100_000, + burnRateCents: 5_000, + derivation: expect.objectContaining({ + baseBurnRateCents: 6_000, + adjustedBurnRateCents: 5_000, + publicHolidayCostDeductionCents: 1_000, + }), + burnStatus: "on_track", + }), + ]); + }); + + it("uses holiday-aware assignment hours for assistant shoring ratio", async () => { + const db = { + project: { + findUnique: vi + .fn() + .mockResolvedValueOnce({ + id: "project_1", + name: "Holiday Project", + shortCode: "HP", + status: "ACTIVE", + responsiblePerson: null, + }) + .mockResolvedValueOnce({ + id: "project_1", + name: "Holiday Project", + shoringThreshold: 55, + onshoreCountryCode: "DE", + }), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([ + { + resourceId: "res_by", + hoursPerDay: 8, + startDate: new Date("2026-01-06T00:00:00.000Z"), + endDate: new Date("2026-01-06T00:00:00.000Z"), + resource: { + id: "res_by", + availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, + countryId: "country_de", + federalState: "BY", + metroCityId: null, + country: { code: "DE" }, + metroCity: null, + }, + }, + { + resourceId: "res_in", + hoursPerDay: 8, + startDate: new Date("2026-01-06T00:00:00.000Z"), + endDate: new Date("2026-01-06T00:00:00.000Z"), + resource: { + id: "res_in", + availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, + countryId: "country_in", + federalState: null, + metroCityId: null, + country: { code: "IN" }, + metroCity: null, + }, + }, + ]), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + }; + const ctx = createToolContext(db); + + const result = await executeTool( + "get_shoring_ratio", + JSON.stringify({ projectId: "project_1" }), + ctx, + ); + + expect(result.content).toContain("0% onshore (DE), 100% offshore"); + expect(result.content).toContain("IN 100% (1 people)"); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-capacity-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-holiday-capacity-test-helpers.ts new file mode 100644 index 0000000..1f93663 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-capacity-test-helpers.ts @@ -0,0 +1,26 @@ +import { SystemRole } from "@capakraken/shared"; + +import type { ToolContext } from "../router/assistant-tools.js"; + +export function createToolContext( + db: Record, + permissions: string[] = [], + userRole: SystemRole = SystemRole.ADMIN, +): ToolContext { + return { + db: db as ToolContext["db"], + userId: "user_1", + userRole, + permissions: new Set(permissions) as ToolContext["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, + }; +}