From 3607d73b844d30ea761603fc34728baf406f529d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:35:16 +0200 Subject: [PATCH] test(api): cover assistant timeline allocation shifts --- ...s-timeline-allocation-shift-errors.test.ts | 138 ++++++++++++++++++ ...t-tools-timeline-allocation-shifts.test.ts | 67 +++++++++ ...tant-tools-timeline-shifts-test-helpers.ts | 70 +++++++++ 3 files changed, 275 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-allocation-shift-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-allocation-shifts.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-shifts-test-helpers.ts diff --git a/packages/api/src/__tests__/assistant-tools-timeline-allocation-shift-errors.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-allocation-shift-errors.test.ts new file mode 100644 index 0000000..d609f10 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-allocation-shift-errors.test.ts @@ -0,0 +1,138 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; +import { TRPCError } from "@trpc/server"; + +import { + createExistingAssignment, + createToolContext, + executeTool, +} from "./assistant-tools-timeline-shifts-test-helpers.js"; + +describe("assistant timeline allocation shift errors", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns stable allocation-not-found errors when timeline allocation persistence loses the row", async () => { + const existingAssignment = createExistingAssignment(); + const missingDuringUpdate = { + code: "P2025", + message: "Record to update not found", + meta: { modelName: "Assignment" }, + }; + + const batchShiftCtx = createToolContext( + { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(existingAssignment), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + $transaction: vi.fn(async () => { + throw missingDuringUpdate; + }), + }, + [PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + SystemRole.MANAGER, + ); + + const batchShiftResult = await executeTool( + "batch_shift_timeline_allocations", + JSON.stringify({ + allocationIds: ["assignment_1"], + daysDelta: 2, + mode: "move", + }), + batchShiftCtx, + ); + expect(JSON.parse(batchShiftResult.content)).toEqual({ + error: "Allocation not found with the given criteria.", + }); + }); + + it("returns a stable allocation-not-found error for batch timeline shifts without matches", async () => { + const db = { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }; + const ctx = createToolContext( + db, + [PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + SystemRole.MANAGER, + ); + + const result = await executeTool( + "batch_shift_timeline_allocations", + JSON.stringify({ + allocationIds: ["assignment_missing"], + daysDelta: 2, + mode: "move", + }), + ctx, + ); + + expect(result.action).toBeUndefined(); + expect(JSON.parse(result.content)).toEqual({ + error: "Allocation not found with the given criteria.", + }); + }); + + it("returns a stable demand-requirement-not-found error for batch timeline shifts", async () => { + const demandRequirement = { + id: "demand_requirement_1", + startDate: new Date("2026-03-10"), + endDate: new Date("2026-03-14"), + }; + const db = { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(demandRequirement), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(null), + }, + $transaction: vi.fn(async () => { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Demand requirement not found", + }); + }), + }; + const ctx = createToolContext( + db, + [PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + SystemRole.MANAGER, + ); + + const result = await executeTool( + "batch_shift_timeline_allocations", + JSON.stringify({ + allocationIds: ["demand_requirement_missing"], + daysDelta: 3, + mode: "move", + }), + ctx, + ); + + expect(result.action).toBeUndefined(); + expect(JSON.parse(result.content)).toEqual({ + error: "Demand requirement not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-timeline-allocation-shifts.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-allocation-shifts.test.ts new file mode 100644 index 0000000..364f92e --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-allocation-shifts.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; + +import { + createExistingAssignment, + createToolContext, + executeTool, +} from "./assistant-tools-timeline-shifts-test-helpers.js"; + +describe("assistant timeline allocation shift tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("batch-shifts timeline allocations through the real timeline router mutation", async () => { + const existingAssignment = createExistingAssignment(); + + const db = { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(existingAssignment), + update: vi.fn().mockResolvedValue({ + ...existingAssignment, + startDate: new Date("2026-03-18"), + endDate: new Date("2026-03-22"), + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), + }; + const ctx = createToolContext( + db, + [PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + SystemRole.MANAGER, + ); + + const result = await executeTool( + "batch_shift_timeline_allocations", + JSON.stringify({ + allocationIds: ["assignment_1"], + daysDelta: 2, + mode: "move", + }), + ctx, + ); + + expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + count: 1, + }), + ); + expect(db.assignment.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: "assignment_1" }, + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-timeline-shifts-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-timeline-shifts-test-helpers.ts new file mode 100644 index 0000000..dc60832 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-shifts-test-helpers.ts @@ -0,0 +1,70 @@ +import { AllocationStatus } from "@capakraken/shared"; +import { vi } from "vitest"; + +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 as executeAssistantTool } from "../router/assistant-tools.js"; + +export { createToolContext } from "./assistant-tools-advanced-timeline-test-helpers.js"; +export const executeTool = executeAssistantTool; + +export function createExistingAssignment() { + return { + id: "assignment_1", + demandRequirementId: null, + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-03-16"), + endDate: new Date("2026-03-20"), + hoursPerDay: 4, + percentage: 50, + role: "Compositor", + roleId: "role_comp", + dailyCostCents: 20000, + status: AllocationStatus.PROPOSED, + metadata: {}, + createdAt: new Date("2026-03-13"), + updatedAt: new Date("2026-03-13"), + resource: { + id: "resource_1", + displayName: "Alice", + eid: "E-001", + lcrCents: 5000, + availability: { + monday: 8, + tuesday: 8, + wednesday: 8, + thursday: 8, + friday: 8, + saturday: 0, + sunday: 0, + }, + }, + project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, + roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, + demandRequirement: null, + }; +}