From 705b57068451fd4c83ff6e0d6838cd163c294c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:34:18 +0200 Subject: [PATCH] test(api): cover assistant timeline quick assign --- ...tools-timeline-quick-assign-errors.test.ts | 153 ++++++++++++++++++ ...ools-timeline-quick-assign-success.test.ts | 66 ++++++++ ...ools-timeline-quick-assign-test-helpers.ts | 91 +++++++++++ 3 files changed, 310 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-quick-assign-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-quick-assign-success.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-timeline-quick-assign-test-helpers.ts diff --git a/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-errors.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-errors.test.ts new file mode 100644 index 0000000..32cffc8 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-errors.test.ts @@ -0,0 +1,153 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { TRPCError } from "@trpc/server"; + +import { + buildBaseProject, + buildBaseResource, + createQuickAssignInput, + createToolContext, + executeTool, + quickAssignPermissions, + quickAssignRole, +} from "./assistant-tools-timeline-quick-assign-test-helpers.js"; + +describe("assistant timeline quick-assign error tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable conflict error for quick-assign timeline mutations", async () => { + const db = { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: vi.fn().mockRejectedValue( + new TRPCError({ + code: "CONFLICT", + message: "Resource is already assigned to this project with overlapping dates", + }), + ), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), + }; + const ctx = createToolContext(db, quickAssignPermissions, quickAssignRole); + + const result = await executeTool( + "quick_assign_timeline_resource", + JSON.stringify(createQuickAssignInput()), + ctx, + ); + + expect(result.action).toBeUndefined(); + expect(JSON.parse(result.content)).toEqual({ + error: "Resource is already assigned to this project with overlapping dates", + }); + }); + + it("returns stable not-found errors when quick-assign timeline targets disappear mid-mutation", async () => { + const cases = [ + { + name: "missing project", + tx: { + project: { + findUnique: vi.fn().mockResolvedValue(null), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: vi.fn(), + }, + }, + expected: "Project not found with the given criteria.", + }, + { + name: "missing resource", + tx: { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: vi.fn(), + }, + }, + expected: "Resource not found with the given criteria.", + }, + ] as const; + + for (const testCase of cases) { + const db = { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: vi.fn(), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + $transaction: vi.fn(async (callback: (tx: typeof testCase.tx) => unknown) => callback(testCase.tx)), + }; + const ctx = createToolContext(db, quickAssignPermissions, quickAssignRole); + + const result = await executeTool( + "quick_assign_timeline_resource", + JSON.stringify(createQuickAssignInput()), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ error: testCase.expected }); + } + }); + + it("returns a stable validation error for quick-assign timeline date ranges", async () => { + const ctx = createToolContext( + { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + }, + quickAssignPermissions, + quickAssignRole, + ); + + const result = await executeTool( + "quick_assign_timeline_resource", + JSON.stringify(createQuickAssignInput({ + startDate: "2026-03-20", + endDate: "2026-03-16", + })), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "End date must be after start date", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-success.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-success.test.ts new file mode 100644 index 0000000..51065ac --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-success.test.ts @@ -0,0 +1,66 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + buildBaseProject, + buildBaseResource, + createCreatedAssignment, + createQuickAssignInput, + createToolContext, + executeTool, + quickAssignPermissions, + quickAssignRole, +} from "./assistant-tools-timeline-quick-assign-test-helpers.js"; + +describe("assistant timeline quick-assign tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("quick-assigns a timeline resource through the real timeline router mutation", async () => { + const createdAssignment = createCreatedAssignment(); + const db = { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + allocation: { + findMany: vi.fn().mockResolvedValue([]), + create: vi.fn(), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: vi.fn().mockResolvedValue(createdAssignment), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), + }; + const ctx = createToolContext(db, quickAssignPermissions, quickAssignRole); + + const result = await executeTool( + "quick_assign_timeline_resource", + JSON.stringify(createQuickAssignInput()), + ctx, + ); + + expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + allocation: expect.objectContaining({ + id: "assignment_quick_1", + projectId: "project_1", + resourceId: "resource_1", + hoursPerDay: 8, + }), + }), + ); + expect(db.assignment.create).toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-test-helpers.ts new file mode 100644 index 0000000..a1273b5 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-quick-assign-test-helpers.ts @@ -0,0 +1,91 @@ +import { PermissionKey, SystemRole } 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 { + buildBaseProject, + buildBaseResource, +} from "./assistant-tools-timeline-allocation-mutation-test-helpers.js"; + +export const quickAssignPermissions = [ + PermissionKey.MANAGE_ALLOCATIONS, + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, +]; + +export const quickAssignRole = SystemRole.MANAGER; + +export function createQuickAssignInput(overrides: Record = {}) { + return { + resourceIdentifier: "resource_1", + projectIdentifier: "project_1", + startDate: "2026-03-16", + endDate: "2026-03-20", + hoursPerDay: 8, + ...overrides, + }; +} + +export function createCreatedAssignment(overrides: Record = {}) { + return { + id: "assignment_quick_1", + demandRequirementId: null, + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-03-16"), + endDate: new Date("2026-03-20"), + hoursPerDay: 8, + percentage: 100, + role: "Team Member", + roleId: null, + dailyCostCents: 40000, + status: "PROPOSED", + metadata: { source: "quickAssign" }, + createdAt: new Date("2026-03-13"), + updatedAt: new Date("2026-03-13"), + resource: { + id: "resource_1", + displayName: "Alice", + eid: "E-001", + lcrCents: 5000, + }, + project: { + id: "project_1", + name: "Gelddruckmaschine", + shortCode: "GDM", + status: "ACTIVE", + responsiblePerson: null, + }, + roleEntity: null, + demandRequirement: null, + ...overrides, + }; +} + +export const executeTool = executeAssistantTool;