From 95940f005b0fc1995b0d918c8b6fefa8ae75bf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:41:17 +0200 Subject: [PATCH] test(api): cover assistant budget status --- .../assistant-tools-budget-status.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-budget-status.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-budget-status.test.ts b/packages/api/src/__tests__/assistant-tools-budget-status.test.ts new file mode 100644 index 0000000..1299b18 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-budget-status.test.ts @@ -0,0 +1,127 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { listAssignmentBookings } from "@capakraken/application"; +import { PermissionKey, 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-insights-scenarios-test-helpers.js"; + +describe("assistant budget status tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(listAssignmentBookings).mockResolvedValue([]); + }); + + it("routes budget status reads through the timeline and project router paths", async () => { + vi.mocked(listAssignmentBookings).mockResolvedValue([ + { + projectId: "project_1", + status: "CONFIRMED", + dailyCostCents: 10_000, + hoursPerDay: 8, + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-02T00:00:00.000Z"), + project: { + id: "project_1", + status: "ACTIVE", + }, + }, + { + projectId: "project_1", + status: "PROPOSED", + dailyCostCents: 5_000, + hoursPerDay: 8, + startDate: new Date("2026-04-03T00:00:00.000Z"), + endDate: new Date("2026-04-03T00:00:00.000Z"), + project: { + id: "project_1", + status: "ACTIVE", + }, + }, + ] as Awaited>); + + const db = { + project: { + findUnique: vi + .fn() + .mockResolvedValueOnce({ + id: "project_1", + name: "Gelddruckmaschine", + shortCode: "GDM", + status: "ACTIVE", + responsiblePerson: "Larissa", + }) + .mockResolvedValueOnce({ + id: "project_1", + name: "Gelddruckmaschine", + shortCode: "GDM", + budgetCents: 100_000, + winProbability: 80, + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-30T00:00:00.000Z"), + }), + findFirst: vi.fn(), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + }, + }; + const ctx = createToolContext(db, { + userRole: SystemRole.CONTROLLER, + permissions: [PermissionKey.VIEW_COSTS], + }); + + const result = await executeTool( + "get_budget_status", + JSON.stringify({ projectId: "project_1" }), + ctx, + ); + + expect(vi.mocked(listAssignmentBookings)).toHaveBeenCalledWith(db, { + projectIds: ["project_1"], + }); + expect(JSON.parse(result.content)).toEqual({ + project: "Gelddruckmaschine", + code: "GDM", + budget: "1.000,00 EUR", + confirmed: "200,00 EUR", + proposed: "50,00 EUR", + allocated: "250,00 EUR", + remaining: "750,00 EUR", + utilization: "25.0%", + winWeighted: "200,00 EUR", + }); + }); +});