From 254f2caa94aaebbf96bc93b0da7cdb3161ae8b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:44:53 +0200 Subject: [PATCH] test(api): cover assistant timeline resource selection --- ...-tools-timeline-resource-selection.test.ts | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-resource-selection.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-timeline-resource-selection.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-resource-selection.test.ts new file mode 100644 index 0000000..ec50a4a --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-resource-selection.test.ts @@ -0,0 +1,210 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../sse/event-bus.js", () => ({ + emitAllocationCreated: vi.fn(), + emitAllocationDeleted: vi.fn(), + emitAllocationUpdated: vi.fn(), + emitProjectShifted: vi.fn(), +})); + +vi.mock("../lib/budget-alerts.js", () => ({ + checkBudgetThresholds: vi.fn(), +})); + +vi.mock("../lib/cache.js", () => ({ + invalidateDashboardCache: vi.fn(), +})); + +import { executeTool } from "../router/assistant-tools.js"; + +import { createToolContext } from "./assistant-tools-advanced-timeline-test-helpers.js"; + +describe("assistant timeline resource selection tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("finds the best project resource with holiday-aware remaining capacity and LCR ranking", async () => { + const assignmentFindMany = vi + .fn() + .mockResolvedValueOnce([ + { + resourceId: "res_carol", + hoursPerDay: 2, + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-16T00:00:00.000Z"), + status: "PROPOSED", + resource: { + id: "res_carol", + eid: "carol.danvers", + displayName: "Carol Danvers", + chapter: "Delivery", + lcrCents: 7664, + availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, + countryId: "country_de", + federalState: "HH", + metroCityId: "city_hamburg", + country: { code: "DE", name: "Deutschland" }, + metroCity: { name: "Hamburg" }, + areaRole: { name: "Artist" }, + }, + }, + { + resourceId: "res_steve", + hoursPerDay: 4, + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-16T00:00:00.000Z"), + status: "CONFIRMED", + resource: { + id: "res_steve", + eid: "steve.rogers", + displayName: "Steve Rogers", + chapter: "Delivery", + lcrCents: 13377, + availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, + countryId: "country_de", + federalState: "BY", + metroCityId: "city_augsburg", + country: { code: "DE", name: "Deutschland" }, + metroCity: { name: "Augsburg" }, + areaRole: { name: "Artist" }, + }, + }, + ]) + .mockResolvedValueOnce([ + { + resourceId: "res_carol", + projectId: "project_lari", + hoursPerDay: 2, + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-16T00:00:00.000Z"), + status: "PROPOSED", + project: { name: "Gelddruckmaschine", shortCode: "LARI" }, + }, + { + resourceId: "res_steve", + projectId: "project_lari", + hoursPerDay: 4, + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-16T00:00:00.000Z"), + status: "CONFIRMED", + project: { name: "Gelddruckmaschine", shortCode: "LARI" }, + }, + ]); + + const ctx = createToolContext( + { + project: { + findUnique: vi + .fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "project_lari", + name: "Gelddruckmaschine", + shortCode: "LARI", + status: "ACTIVE", + responsiblePerson: "Larissa Joos", + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-16T00:00:00.000Z"), + }) + .mockResolvedValueOnce({ + id: "project_lari", + name: "Gelddruckmaschine", + shortCode: "LARI", + status: "ACTIVE", + responsiblePerson: "Larissa Joos", + }), + findFirst: vi.fn(), + }, + assignment: { + findMany: assignmentFindMany, + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + }, + [ + PermissionKey.VIEW_COSTS, + PermissionKey.VIEW_PLANNING, + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, + ], + ); + + const result = await executeTool( + "find_best_project_resource", + JSON.stringify({ + projectIdentifier: "LARI", + startDate: "2026-01-05", + endDate: "2026-01-16", + minHoursPerDay: 3, + rankingMode: "lowest_lcr", + }), + ctx, + ); + + const parsed = JSON.parse(result.content) as { + project: { shortCode: string }; + candidateCount: number; + bestMatch: { + name: string; + remainingHoursPerDay: number; + lcrCents: number | null; + federalState: string | null; + metroCity: string | null; + baseAvailableHours: number; + holidaySummary: { count: number }; + }; + candidates: Array<{ + name: string; + remainingHoursPerDay: number; + workingDays: number; + baseAvailableHours: number; + holidaySummary: { count: number; hoursDeduction: number }; + capacityBreakdown: { holidayHoursDeduction: number }; + }>; + }; + + expect(parsed.project.shortCode).toBe("LARI"); + expect(parsed.candidateCount).toBe(2); + expect(parsed.bestMatch).toEqual( + expect.objectContaining({ + name: "Carol Danvers", + remainingHoursPerDay: 6, + lcrCents: 7664, + federalState: "HH", + metroCity: "Hamburg", + baseAvailableHours: 80, + holidaySummary: expect.objectContaining({ count: 0 }), + }), + ); + expect(parsed.candidates).toEqual([ + expect.objectContaining({ + name: "Carol Danvers", + remainingHoursPerDay: 6, + workingDays: 10, + baseAvailableHours: 80, + holidaySummary: expect.objectContaining({ count: 0, hoursDeduction: 0 }), + capacityBreakdown: expect.objectContaining({ holidayHoursDeduction: 0 }), + }), + expect.objectContaining({ + name: "Steve Rogers", + remainingHoursPerDay: 4, + workingDays: 9, + baseAvailableHours: 80, + holidaySummary: expect.objectContaining({ count: 1, hoursDeduction: 8 }), + capacityBreakdown: expect.objectContaining({ holidayHoursDeduction: 8 }), + }), + ]); + }); +});