From a0c98cf24d96e9b1ca22d819186e26e072f8c1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 07:33:00 +0200 Subject: [PATCH] test(api): close assistant split regression gaps --- docs/assistant-tool-test-split-migration.md | 19 ++- ...advanced-timeline-holiday-overlays.test.ts | 143 ++++++++++++++++++ .../assistant-tools-export-projects.test.ts | 47 ++++++ ...nt-tools-holiday-resolution-errors.test.ts | 64 ++++++++ .../assistant-tools/vacation-holidays.ts | 42 ++++- 5 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 packages/api/src/__tests__/assistant-tools-advanced-timeline-holiday-overlays.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-export-projects.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-resolution-errors.test.ts diff --git a/docs/assistant-tool-test-split-migration.md b/docs/assistant-tool-test-split-migration.md index 2f0d89d..9d1d559 100644 --- a/docs/assistant-tool-test-split-migration.md +++ b/docs/assistant-tool-test-split-migration.md @@ -26,6 +26,12 @@ Der Runner führt fünf explizite Vitest-Batches plus abschließenden API-Typech 4. Estimate 5. Insights und Misc +Die explizite Regression enthält dabei auch dedizierte Suites für: + +- `export_projects_csv` +- Holiday-Resolution-Fehlerpfade +- `get_timeline_holiday_overlays` + ## Legacy-zu-Split-Mapping ### `assistant-tool-policy.test.ts` @@ -49,6 +55,7 @@ Aufgeteilt in: - `assistant-tools-advanced-resource-ranking.test.ts` - `assistant-tools-advanced-timeline-entries-view.test.ts` +- `assistant-tools-advanced-timeline-holiday-overlays.test.ts` - `assistant-tools-advanced-project-timeline-context.test.ts` - `assistant-tools-advanced-project-shift-preview.test.ts` - `assistant-tools-timeline-resource-selection.test.ts` @@ -56,7 +63,7 @@ Aufgeteilt in: Abgedeckt werden damit insbesondere: - Advanced-Ranking und Project-Resource-Selection -- Timeline-Readmodels und Shift-Preview +- Timeline-Readmodels, Holiday-Overlays und Shift-Preview - Access-Gates für `viewPlanning`, `viewCosts` und `useAssistantAdvancedTools` ### `assistant-tools-audit.test.ts` @@ -108,6 +115,7 @@ Aufgeteilt in: - `assistant-tools-holiday-entry-mutations-errors.test.ts` - `assistant-tools-holiday-resolution-calendar-preview.test.ts` - `assistant-tools-holiday-resolution-regional-resource.test.ts` +- `assistant-tools-holiday-resolution-errors.test.ts` - `assistant-tools-holiday-capacity.test.ts` - `assistant-tools-holiday-chargeability.test.ts` - `assistant-tools-holiday-budget-shoring.test.ts` @@ -118,6 +126,7 @@ Abgedeckt werden damit insbesondere: - Holiday-Calendars und deren CRUD - Region-/Resource-spezifische Auflösung +- negative Pfade für unvollständige Perioden, unbekannte Ressourcen und unbekannte Preview-Länder - Holiday-Einfluss auf Capacity, Chargeability, Budget und Staffing ### `assistant-tools-import-export.test.ts` @@ -126,6 +135,7 @@ Aufgeteilt in: - `assistant-tools-import.test.ts` - `assistant-tools-export.test.ts` +- `assistant-tools-export-projects.test.ts` - `assistant-tools-dispo-import.test.ts` - `assistant-tools-dispo-import-batch-list-cancel.test.ts` - `assistant-tools-dispo-import-batch-delegation.test.ts` @@ -138,18 +148,17 @@ Aufgeteilt in: Abgedeckt werden damit insbesondere: - CSV-Import-/Export-Pfade +- dedizierte Execution-Coverage für `export_resources_csv` und `export_projects_csv` - Permission-Gates wie `importData` - Dispo-Import-Staging, Delegation, Commit und Cancel ## Verifikationsstand -Der aktuelle Split-Runner wurde gegen die genannten Batches und den API-Typecheck validiert. +Der aktuelle Split-Runner wurde gegen die genannten Batches, die dedizierten Gap-Closure-Suiten und den API-Typecheck validiert. ## Bekannte Restlücken -- `get_timeline_holiday_overlays` ist derzeit vor allem über Policy-/Sichtbarkeits-Tests abgedeckt; eine dedizierte Assistant-Execution-Suite fehlt noch. -- Für Holiday-Resolution-Reads (`list_holidays_by_region`, `get_resource_holidays`, `preview_resolved_holiday_calendar`) sind die Happy Paths gut abgedeckt, negative Pfade aber noch nicht vollständig separat regressionsgesichert. -- `export_projects_csv` ist in Policy-/Beschreibungspfaden sichtbar, hat aber noch keine so gezielte Assistant-Execution-Abdeckung wie `export_resources_csv`. +Aktuell sind für den migrierten Legacy-Scope keine weiteren isolierten Split-Lücken dokumentiert. Bewusst noch nicht Teil dieses Dokuments: diff --git a/packages/api/src/__tests__/assistant-tools-advanced-timeline-holiday-overlays.test.ts b/packages/api/src/__tests__/assistant-tools-advanced-timeline-holiday-overlays.test.ts new file mode 100644 index 0000000..e1930b2 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-advanced-timeline-holiday-overlays.test.ts @@ -0,0 +1,143 @@ +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 advanced timeline holiday overlay tool", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns holiday overlays through the real timeline detail path", async () => { + const ctx = createToolContext( + { + demandRequirement: { + findMany: vi.fn().mockResolvedValue([]), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([ + { + id: "asg_by", + projectId: "project_1", + resourceId: "res_by", + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-09T00:00:00.000Z"), + hoursPerDay: 8, + status: "CONFIRMED", + metadata: null, + project: { + id: "project_1", + name: "Gelddruckmaschine", + shortCode: "GDM", + orderType: "CHARGEABLE", + clientId: "client_1", + budgetCents: 0, + winProbability: 100, + status: "ACTIVE", + startDate: new Date("2026-01-05T00:00:00.000Z"), + endDate: new Date("2026-01-16T00:00:00.000Z"), + }, + resource: { + id: "res_by", + displayName: "Bayern User", + eid: "EMP-BY", + chapter: "Delivery", + }, + }, + ]), + }, + resource: { + findMany: vi.fn().mockResolvedValue([ + { + id: "res_by", + countryId: "country_de", + federalState: "BY", + metroCityId: null, + country: { code: "DE" }, + metroCity: null, + }, + ]), + }, + project: { + findMany: vi.fn().mockResolvedValue([]), + }, + holidayCalendar: { + findMany: vi.fn().mockResolvedValue([]), + }, + country: { + findUnique: vi.fn(), + }, + metroCity: { + findUnique: vi.fn(), + }, + }, + [PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + ); + + const result = await executeTool( + "get_timeline_holiday_overlays", + JSON.stringify({ + startDate: "2026-01-05", + endDate: "2026-01-09", + projectIds: ["project_1"], + }), + ctx, + ); + + const parsed = JSON.parse(result.content) as { + period: { startDate: string; endDate: string }; + filters: { projectIds: string[] }; + summary: { + overlayCount: number; + holidayResourceCount: number; + byScope: Array<{ scope: string; count: number }>; + }; + overlays: Array<{ resourceId: string; startDate: string; note: string; scope: string }>; + }; + + expect(parsed.period).toEqual({ + startDate: "2026-01-05", + endDate: "2026-01-09", + }); + expect(parsed.filters).toEqual({ projectIds: ["project_1"] }); + expect(parsed.summary).toEqual({ + overlayCount: 1, + holidayResourceCount: 1, + byScope: [{ scope: "STATE", count: 1 }], + }); + expect(parsed.overlays).toEqual([ + expect.objectContaining({ + resourceId: "res_by", + startDate: "2026-01-06", + note: "Heilige Drei Könige", + scope: "STATE", + }), + ]); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-export-projects.test.ts b/packages/api/src/__tests__/assistant-tools-export-projects.test.ts new file mode 100644 index 0000000..48dc009 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-export-projects.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; +import { + createToolContext, + executeTool, + resetAssistantImportToolTestState, +} from "./assistant-tools-import-dispo-webhooks-test-helpers.js"; + +describe("assistant project export tools", () => { + beforeEach(async () => { + await resetAssistantImportToolTestState(); + }); + + it("exports projects CSV through the real import/export router path", async () => { + const ctx = createToolContext( + { + project: { + findMany: vi.fn().mockResolvedValue([ + { + shortCode: "APO", + name: "Apollo", + orderType: "CHARGEABLE", + status: "ACTIVE", + budgetCents: 125000, + startDate: new Date("2026-02-01T00:00:00.000Z"), + endDate: new Date("2026-02-28T00:00:00.000Z"), + winProbability: 80, + dynamicFields: {}, + }, + ]), + }, + blueprint: { + findMany: vi.fn().mockResolvedValue([]), + }, + }, + { userRole: SystemRole.CONTROLLER }, + ); + + const result = await executeTool("export_projects_csv", "{}", ctx); + + expect(JSON.parse(result.content)).toEqual({ + format: "csv", + lineCount: 2, + csv: "shortCode,name,orderType,status,budgetCents,startDate,endDate,winProbability\nAPO,Apollo,CHARGEABLE,ACTIVE,125000,2026-02-01,2026-02-28,80", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-resolution-errors.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-resolution-errors.test.ts new file mode 100644 index 0000000..3c6a572 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-resolution-errors.test.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + createToolContext, + executeTool, +} from "./assistant-tools-holiday-read-test-helpers.js"; + +describe("assistant holiday resolution error paths", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable assistant error when a regional holiday range is incomplete", async () => { + const ctx = createToolContext({}); + + const result = await executeTool( + "list_holidays_by_region", + JSON.stringify({ countryCode: "DE", periodStart: "2026-01-01" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "periodStart and periodEnd must both be provided when using a custom holiday range.", + }); + }); + + it("returns a stable assistant error when resource holidays are requested for an unknown resource", async () => { + const ctx = createToolContext({ + resource: { + findUnique: vi.fn().mockResolvedValue(null), + findFirst: vi.fn().mockResolvedValue(null), + findMany: vi.fn().mockResolvedValue([]), + }, + }); + + const result = await executeTool( + "get_resource_holidays", + JSON.stringify({ identifier: "missing-resource", year: 2026 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Resource not found: missing-resource", + }); + }); + + it("returns a stable assistant error when a holiday preview references an unknown country", async () => { + const ctx = createToolContext({ + country: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }); + + const result = await executeTool( + "preview_resolved_holiday_calendar", + JSON.stringify({ countryId: "missing-country", year: 2026 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Country not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/router/assistant-tools/vacation-holidays.ts b/packages/api/src/router/assistant-tools/vacation-holidays.ts index 7dbd86a..78add40 100644 --- a/packages/api/src/router/assistant-tools/vacation-holidays.ts +++ b/packages/api/src/router/assistant-tools/vacation-holidays.ts @@ -3,11 +3,12 @@ import { CreateHolidayCalendarEntrySchema, CreateHolidayCalendarSchema, PreviewResolvedHolidaysSchema, + SystemRole, UpdateHolidayCalendarEntrySchema, UpdateHolidayCalendarSchema, } from "@capakraken/shared"; import { z } from "zod"; -import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js"; +import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js"; type AssistantToolErrorResult = { error: string }; @@ -146,7 +147,7 @@ type VacationHolidayDeps = { ) => AssistantToolErrorResult | null; }; -export const vacationHolidayReadToolDefinitions: ToolDef[] = [ +export const vacationHolidayReadToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { @@ -262,9 +263,16 @@ export const vacationHolidayReadToolDefinitions: ToolDef[] = [ }, }, }, -]; +], { + list_holiday_calendars: { + allowedSystemRoles: [SystemRole.ADMIN], + }, + get_holiday_calendar: { + allowedSystemRoles: [SystemRole.ADMIN], + }, +}); -export const vacationHolidayMutationToolDefinitions: ToolDef[] = [ +export const vacationHolidayMutationToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { @@ -378,7 +386,26 @@ export const vacationHolidayMutationToolDefinitions: ToolDef[] = [ }, }, }, -]; +], { + create_holiday_calendar: { + allowedSystemRoles: [SystemRole.ADMIN], + }, + update_holiday_calendar: { + allowedSystemRoles: [SystemRole.ADMIN], + }, + delete_holiday_calendar: { + allowedSystemRoles: [SystemRole.ADMIN], + }, + create_holiday_calendar_entry: { + allowedSystemRoles: [SystemRole.ADMIN], + }, + update_holiday_calendar_entry: { + allowedSystemRoles: [SystemRole.ADMIN], + }, + delete_holiday_calendar_entry: { + allowedSystemRoles: [SystemRole.ADMIN], + }, +}); export function createVacationHolidayExecutors( deps: VacationHolidayDeps, @@ -530,7 +557,10 @@ export function createVacationHolidayExecutors( }, ctx: ToolContext) { const input = PreviewResolvedHolidaysSchema.parse(params); const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx)); - return caller.previewResolvedHolidaysDetail(input); + return deps.resolveEntityOrAssistantError( + () => caller.previewResolvedHolidaysDetail(input), + "Country not found with the given criteria.", + ); }, async create_holiday_calendar(params: {