diff --git a/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-errors.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-errors.test.ts new file mode 100644 index 0000000..d286c9c --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-errors.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + batchQuickAssignPermissions, + batchQuickAssignRole, + buildBaseProject, + buildBaseResource, + createBatchQuickAssignInput, + createToolContext, + executeTool, +} from "./assistant-tools-timeline-batch-quick-assign-test-helpers.js"; + +describe("assistant timeline batch quick-assign error tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a structured batch assignment resolver error for an unknown resource", async () => { + const db = { + project: { + findUnique: vi.fn().mockResolvedValue({ + ...buildBaseProject(), + startDate: new Date("2026-03-16"), + endDate: new Date("2026-03-20"), + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(null), + findFirst: vi.fn().mockResolvedValue(null), + findMany: vi.fn().mockResolvedValue([]), + }, + assignment: { + create: vi.fn(), + }, + }; + const ctx = createToolContext(db, batchQuickAssignPermissions, batchQuickAssignRole); + + const result = await executeTool( + "batch_quick_assign_timeline_resources", + JSON.stringify( + createBatchQuickAssignInput([ + { + resourceIdentifier: "missing_resource", + }, + ]), + ), + ctx, + ); + + expect(result.action).toBeUndefined(); + expect(JSON.parse(result.content)).toEqual({ + error: "assignments[0].resourceIdentifier: Resource not found: missing_resource", + field: "assignments[0].resourceIdentifier", + index: 0, + }); + expect(db.assignment.create).not.toHaveBeenCalled(); + }); + + it("returns a stable validation error for batch timeline mutation date ranges", async () => { + const ctx = createToolContext( + { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + }, + batchQuickAssignPermissions, + batchQuickAssignRole, + ); + + const result = await executeTool( + "batch_quick_assign_timeline_resources", + JSON.stringify( + createBatchQuickAssignInput([ + { + 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-batch-quick-assign-success.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-success.test.ts new file mode 100644 index 0000000..2f24ba4 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-success.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + batchQuickAssignPermissions, + batchQuickAssignRole, + buildBaseProject, + buildBaseResource, + createBatchQuickAssignInput, + createToolContext, + executeTool, +} from "./assistant-tools-timeline-batch-quick-assign-test-helpers.js"; + +describe("assistant timeline batch quick-assign tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("batch quick-assigns timeline resources through the real timeline router mutation", async () => { + const db = { + project: { + findUnique: vi.fn().mockResolvedValue(buildBaseProject()), + }, + resource: { + findUnique: vi + .fn() + .mockImplementation(async ({ where }: { where: { id: string } }) => buildBaseResource(where.id)), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: vi + .fn() + .mockResolvedValueOnce({ id: "assignment_batch_1" }) + .mockResolvedValueOnce({ id: "assignment_batch_2" }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), + }; + const ctx = createToolContext(db, batchQuickAssignPermissions, batchQuickAssignRole); + + const result = await executeTool( + "batch_quick_assign_timeline_resources", + JSON.stringify(createBatchQuickAssignInput()), + ctx, + ); + + expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + count: 2, + }), + ); + expect(db.assignment.create).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-test-helpers.ts new file mode 100644 index 0000000..255b40c --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-batch-quick-assign-test-helpers.ts @@ -0,0 +1,72 @@ +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 batchQuickAssignPermissions = [ + PermissionKey.MANAGE_ALLOCATIONS, + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, +]; + +export const batchQuickAssignRole = SystemRole.MANAGER; + +export function createBatchQuickAssignInput( + overrides: Array> = [], +) { + const baseAssignments = [ + { + resourceIdentifier: "resource_1", + projectIdentifier: "project_1", + startDate: "2026-03-16", + endDate: "2026-03-20", + hoursPerDay: 8, + }, + { + resourceIdentifier: "resource_2", + projectIdentifier: "project_1", + startDate: "2026-03-23", + endDate: "2026-03-27", + hoursPerDay: 6, + }, + ]; + + return { + assignments: baseAssignments.map((assignment, index) => ({ + ...assignment, + ...(overrides[index] ?? {}), + })), + }; +} + +export const executeTool = executeAssistantTool;