diff --git a/packages/api/src/__tests__/assistant-tools-timeline-inline-allocation-update-errors.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-inline-allocation-update-errors.test.ts new file mode 100644 index 0000000..a013828 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-inline-allocation-update-errors.test.ts @@ -0,0 +1,165 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } 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"; +import { + baseAvailability, + buildBaseAssignment, +} from "./assistant-tools-timeline-allocation-mutation-test-helpers.js"; + +describe("assistant timeline inline allocation update error tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable validation error for inline timeline mutation date ranges", async () => { + const ctx = createToolContext( + { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(buildBaseAssignment()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue({ + id: "resource_1", + lcrCents: 5000, + availability: baseAvailability, + }), + }, + }, + [PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + SystemRole.MANAGER, + ); + + const result = await executeTool( + "update_timeline_allocation_inline", + JSON.stringify({ + allocationId: "assignment_1", + startDate: "2026-03-20", + endDate: "2026-03-16", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "End date must be after start date", + }); + }); + + it("returns a stable allocation-not-found error for inline timeline updates", 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( + "update_timeline_allocation_inline", + JSON.stringify({ + allocationId: "assignment_missing", + hoursPerDay: 6, + }), + ctx, + ); + + expect(result.action).toBeUndefined(); + expect(JSON.parse(result.content)).toEqual({ + error: "Allocation not found with the given criteria.", + }); + }); + + it("returns stable allocation-not-found errors when timeline allocation persistence loses the row", async () => { + const missingDuringUpdate = { + code: "P2025", + message: "Record to update not found", + meta: { modelName: "Assignment" }, + }; + + const updateInlineCtx = createToolContext( + { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(buildBaseAssignment()), + }, + resource: { + findUnique: vi.fn().mockResolvedValue({ + id: "resource_1", + lcrCents: 5000, + availability: baseAvailability, + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({}), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + $transaction: vi.fn(async () => { + throw missingDuringUpdate; + }), + }, + [PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], + SystemRole.MANAGER, + ); + + const updateInlineResult = await executeTool( + "update_timeline_allocation_inline", + JSON.stringify({ + allocationId: "assignment_1", + hoursPerDay: 6, + }), + updateInlineCtx, + ); + + expect(JSON.parse(updateInlineResult.content)).toEqual({ + error: "Allocation not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-timeline-inline-allocation-update-success.test.ts b/packages/api/src/__tests__/assistant-tools-timeline-inline-allocation-update-success.test.ts new file mode 100644 index 0000000..c47353b --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-timeline-inline-allocation-update-success.test.ts @@ -0,0 +1,112 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { AllocationStatus, PermissionKey, SystemRole } 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"; +import { + buildBaseAssignment, + buildBaseResource, +} from "./assistant-tools-timeline-allocation-mutation-test-helpers.js"; + +describe("assistant timeline inline allocation update success tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("updates timeline allocations inline through the real timeline router mutation", async () => { + const existingAssignment = { + ...buildBaseAssignment(), + status: AllocationStatus.PROPOSED, + }; + const updatedAssignment = { + ...existingAssignment, + hoursPerDay: 6, + endDate: new Date("2026-03-21"), + percentage: 75, + dailyCostCents: 30000, + metadata: { includeSaturday: true }, + updatedAt: new Date("2026-03-14"), + }; + + const db = { + allocation: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + assignment: { + findUnique: vi.fn().mockResolvedValue(existingAssignment), + update: vi.fn().mockResolvedValue(updatedAssignment), + }, + resource: { + findUnique: vi.fn().mockResolvedValue(buildBaseResource()), + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + 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( + "update_timeline_allocation_inline", + JSON.stringify({ + allocationId: "assignment_1", + hoursPerDay: 6, + endDate: "2026-03-21", + includeSaturday: true, + }), + 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_1", + hoursPerDay: 6, + endDate: "2026-03-21", + }), + }), + ); + expect(db.assignment.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: "assignment_1" }, + }), + ); + }); +});