From e1228244e907af07bbe90078977e85d9ddf7d33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:29:07 +0200 Subject: [PATCH] test(api): cover assistant demand tools --- ...nt-tools-demand-create-race-errors.test.ts | 137 ++++++++++++ ...nt-tools-demand-create-role-errors.test.ts | 110 +++++++++ ...istant-tools-demand-create-success.test.ts | 126 +++++++++++ ...istant-tools-demand-create-test-helpers.ts | 40 ++++ ...assistant-tools-demand-fill-errors.test.ts | 128 +++++++++++ .../assistant-tools-demand-fill.test.ts | 209 ++++++++++++++++++ 6 files changed, 750 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-demand-create-race-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-demand-create-role-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-demand-create-success.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-demand-create-test-helpers.ts create mode 100644 packages/api/src/__tests__/assistant-tools-demand-fill-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-demand-fill.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-demand-create-race-errors.test.ts b/packages/api/src/__tests__/assistant-tools-demand-create-race-errors.test.ts new file mode 100644 index 0000000..e8f00e7 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-demand-create-race-errors.test.ts @@ -0,0 +1,137 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; +import { + createToolContext, + executeTool, +} from "./assistant-tools-demand-create-test-helpers.js"; + +describe("assistant demand create tool - race errors", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable assistant error when the demand project disappears before creation", async () => { + const tx = { + project: { + findUnique: vi.fn().mockResolvedValue(null), + }, + demandRequirement: { + create: vi.fn(), + }, + auditLog: { + create: vi.fn(), + }, + }; + const ctx = createToolContext( + { + project: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + status: "ACTIVE", + responsiblePerson: "Peter Parker", + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + role: { + findUnique: vi.fn().mockResolvedValue({ id: "role_1", name: "Designer" }), + findFirst: vi.fn().mockResolvedValue(null), + }, + $transaction: vi.fn().mockImplementation( + async (callback: (inner: typeof tx) => Promise) => callback(tx), + ), + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "create_demand", + JSON.stringify({ + projectId: "PROJ-1", + roleName: "Designer", + headcount: 2, + hoursPerDay: 6, + startDate: "2026-05-01", + endDate: "2026-05-15", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Project not found with the given criteria.", + }); + }); + + it("returns a stable assistant error when the selected demand role disappears before creation", async () => { + const tx = { + project: { + findUnique: vi.fn().mockResolvedValue({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + budgetCents: 0, + }), + }, + demandRequirement: { + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "DemandRequirement_roleId_fkey" }, + }), + }, + auditLog: { + create: vi.fn(), + }, + }; + const ctx = createToolContext( + { + project: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + status: "ACTIVE", + responsiblePerson: "Peter Parker", + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + role: { + findUnique: vi.fn().mockResolvedValue({ id: "role_1", name: "Designer" }), + findFirst: vi.fn().mockResolvedValue(null), + }, + $transaction: vi.fn().mockImplementation( + async (callback: (inner: typeof tx) => Promise) => callback(tx), + ), + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "create_demand", + JSON.stringify({ + projectId: "PROJ-1", + roleName: "Designer", + headcount: 2, + hoursPerDay: 6, + startDate: "2026-05-01", + endDate: "2026-05-15", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Role not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-demand-create-role-errors.test.ts b/packages/api/src/__tests__/assistant-tools-demand-create-role-errors.test.ts new file mode 100644 index 0000000..e337b91 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-demand-create-role-errors.test.ts @@ -0,0 +1,110 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; +import { TRPCError } from "@trpc/server"; +import { + createToolContext, + executeTool, +} from "./assistant-tools-demand-create-test-helpers.js"; + +describe("assistant demand create tool - role resolution errors", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable assistant error when the role cannot be resolved during demand creation", async () => { + const ctx = createToolContext( + { + project: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + status: "ACTIVE", + responsiblePerson: "Peter Parker", + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + role: { + findUnique: vi.fn().mockResolvedValue(null), + findFirst: vi.fn().mockResolvedValue(null), + }, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "create_demand", + JSON.stringify({ + projectId: "PROJ-1", + roleName: "Missing Role", + headcount: 2, + hoursPerDay: 6, + startDate: "2026-05-01", + endDate: "2026-05-15", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Role not found: Missing Role", + }); + }); + + it("returns a generic assistant error when role resolution fails internally during demand creation", async () => { + const demandCreate = vi.fn(); + const ctx = createToolContext( + { + project: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + status: "ACTIVE", + responsiblePerson: "Peter Parker", + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + role: { + findUnique: vi.fn().mockRejectedValue( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "role resolver connection exhausted", + }), + ), + }, + demandRequirement: { + create: demandCreate, + }, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "create_demand", + JSON.stringify({ + projectId: "PROJ-1", + roleName: "Designer", + headcount: 2, + hoursPerDay: 6, + startDate: "2026-05-01", + endDate: "2026-05-15", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "The tool could not complete due to an internal error.", + }); + expect(demandCreate).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-demand-create-success.test.ts b/packages/api/src/__tests__/assistant-tools-demand-create-success.test.ts new file mode 100644 index 0000000..7f3b19f --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-demand-create-success.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; +import { + createToolContext, + executeTool, +} from "./assistant-tools-demand-create-test-helpers.js"; + +describe("assistant demand create tool - success", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes demand creation through the real allocation router path and writes an audit log", async () => { + const auditCreate = vi.fn().mockResolvedValue({ id: "audit_1" }); + const notificationCreate = vi.fn().mockResolvedValue({ id: "task_1" }); + const demandCreate = vi.fn().mockResolvedValue({ + id: "demand_1", + projectId: "project_1", + roleId: "role_1", + role: null, + headcount: 2, + hoursPerDay: 6, + percentage: 75, + budgetCents: 0, + status: "PROPOSED", + metadata: {}, + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-05-15T00:00:00.000Z"), + project: { id: "project_1", name: "Project One", shortCode: "PROJ-1" }, + roleEntity: { id: "role_1", name: "Designer", color: "#00AAFF" }, + }); + const tx = { + project: { + findUnique: vi.fn().mockResolvedValue({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + budgetCents: 0, + }), + }, + demandRequirement: { create: demandCreate }, + auditLog: { create: auditCreate }, + }; + const transaction = vi.fn().mockImplementation( + async (callback: (inner: typeof tx) => Promise) => callback(tx), + ); + const projectFindFirst = vi.fn().mockResolvedValue({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + }); + const projectFindUnique = vi.fn().mockResolvedValue({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + budgetCents: 0, + }); + const roleFindFirst = vi.fn().mockResolvedValue({ id: "role_1", name: "Designer" }); + const roleFindUnique = vi.fn().mockResolvedValue({ id: "role_1", name: "Designer" }); + const ctx = createToolContext( + { + project: { + findFirst: projectFindFirst, + findUnique: projectFindUnique, + }, + role: { + findFirst: roleFindFirst, + findUnique: roleFindUnique, + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + }, + demandRequirement: { + findMany: vi.fn().mockResolvedValue([]), + }, + user: { + findMany: vi.fn().mockResolvedValue([{ id: "manager_1" }]), + }, + notification: { + findFirst: vi.fn().mockResolvedValue(null), + create: notificationCreate, + }, + $transaction: transaction, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "create_demand", + JSON.stringify({ + projectId: "PROJ-1", + roleName: "Designer", + headcount: 2, + hoursPerDay: 6, + startDate: "2026-05-01", + endDate: "2026-05-15", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + message: + "Created demand: Designer × 2 for Project One (PROJ-1), 6h/day, 2026-05-01 to 2026-05-15", + demandId: "demand_1", + }), + ); + expect(demandCreate).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + projectId: "project_1", + roleId: "role_1", + headcount: 2, + hoursPerDay: 6, + percentage: 75, + }), + }), + ); + expect(auditCreate).toHaveBeenCalledTimes(1); + expect(notificationCreate).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-demand-create-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-demand-create-test-helpers.ts new file mode 100644 index 0000000..7c99a03 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-demand-create-test-helpers.ts @@ -0,0 +1,40 @@ +import { vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + approveEstimateVersion: vi.fn(), + cloneEstimate: vi.fn(), + commitDispoImportBatch: vi.fn(), + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + createEstimateExport: vi.fn(), + createEstimatePlanningHandoff: vi.fn(), + createEstimateRevision: vi.fn(), + assessDispoImportReadiness: vi.fn(), + loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()), + getDashboardDemand: vi.fn().mockResolvedValue([]), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardOverview: vi.fn(), + getDashboardSkillGapSummary: vi.fn().mockResolvedValue({ + roleGaps: [], + totalOpenPositions: 0, + skillSupplyTop10: [], + resourcesByRole: [], + }), + getDashboardProjectHealth: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + getDashboardTopValueResources: vi.fn().mockResolvedValue([]), + getEstimateById: vi.fn(), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + stageDispoImportBatch: vi.fn(), + submitEstimateVersion: vi.fn(), + updateEstimateDraft: vi.fn(), + }; +}); + +import { executeTool as executeAssistantTool } from "../router/assistant-tools.js"; +import { createToolContext as createAllocationPlanningToolContext } from "./assistant-tools-allocation-planning-test-helpers.js"; + +export const executeTool = executeAssistantTool; +export const createToolContext = createAllocationPlanningToolContext; diff --git a/packages/api/src/__tests__/assistant-tools-demand-fill-errors.test.ts b/packages/api/src/__tests__/assistant-tools-demand-fill-errors.test.ts new file mode 100644 index 0000000..b216a5c --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-demand-fill-errors.test.ts @@ -0,0 +1,128 @@ +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, + approveEstimateVersion: vi.fn(), + cloneEstimate: vi.fn(), + commitDispoImportBatch: vi.fn(), + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + createEstimateExport: vi.fn(), + createEstimatePlanningHandoff: vi.fn(), + createEstimateRevision: vi.fn(), + assessDispoImportReadiness: vi.fn(), + loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()), + getDashboardDemand: vi.fn().mockResolvedValue([]), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardOverview: vi.fn(), + getDashboardSkillGapSummary: vi.fn().mockResolvedValue({ + roleGaps: [], + totalOpenPositions: 0, + skillSupplyTop10: [], + resourcesByRole: [], + }), + getDashboardProjectHealth: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + getDashboardTopValueResources: vi.fn().mockResolvedValue([]), + getEstimateById: vi.fn(), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + stageDispoImportBatch: vi.fn(), + submitEstimateVersion: vi.fn(), + updateEstimateDraft: vi.fn(), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; + +import { createToolContext } from "./assistant-tools-allocation-planning-test-helpers.js"; + +describe("assistant demand fill error mapping", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable assistant error when demand filling cannot resolve the demand", async () => { + const ctx = createToolContext( + { + demandRequirement: { + findUnique: vi.fn().mockResolvedValue(null), + }, + resource: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "resource_1", + displayName: "Carol Danvers", + eid: "EMP-001", + chapter: "Delivery", + isActive: true, + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "fill_demand", + JSON.stringify({ demandId: "demand_missing", resourceId: "resource_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Demand not found with the given criteria.", + }); + }); + + it("returns a stable assistant error when demand filling violates lifecycle preconditions", async () => { + const ctx = createToolContext( + { + demandRequirement: { + findUnique: vi.fn().mockResolvedValue({ + id: "demand_1", + projectId: "project_1", + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-05-15T00:00:00.000Z"), + hoursPerDay: 6, + role: "Designer", + roleId: "role_1", + headcount: 1, + status: "COMPLETED", + metadata: {}, + }), + }, + resource: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ + id: "resource_1", + displayName: "Carol Danvers", + eid: "EMP-001", + chapter: "Delivery", + isActive: true, + }), + findFirst: vi.fn().mockResolvedValue(null), + }, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "fill_demand", + JSON.stringify({ demandId: "demand_1", resourceId: "resource_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Demand cannot be filled in its current status.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-demand-fill.test.ts b/packages/api/src/__tests__/assistant-tools-demand-fill.test.ts new file mode 100644 index 0000000..594796c --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-demand-fill.test.ts @@ -0,0 +1,209 @@ +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, + approveEstimateVersion: vi.fn(), + cloneEstimate: vi.fn(), + commitDispoImportBatch: vi.fn(), + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + createEstimateExport: vi.fn(), + createEstimatePlanningHandoff: vi.fn(), + createEstimateRevision: vi.fn(), + assessDispoImportReadiness: vi.fn(), + loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()), + getDashboardDemand: vi.fn().mockResolvedValue([]), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardOverview: vi.fn(), + getDashboardSkillGapSummary: vi.fn().mockResolvedValue({ + roleGaps: [], + totalOpenPositions: 0, + skillSupplyTop10: [], + resourcesByRole: [], + }), + getDashboardProjectHealth: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + getDashboardTopValueResources: vi.fn().mockResolvedValue([]), + getEstimateById: vi.fn(), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + stageDispoImportBatch: vi.fn(), + submitEstimateVersion: vi.fn(), + updateEstimateDraft: vi.fn(), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; + +import { createToolContext } from "./assistant-tools-allocation-planning-test-helpers.js"; + +describe("assistant demand fill tool", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes demand filling through the real allocation router path", async () => { + const auditCreate = vi.fn().mockResolvedValue({ id: "audit_1" }); + const assignmentCreate = vi.fn().mockResolvedValue({ + id: "assignment_1", + demandRequirementId: "demand_1", + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-05-15T00:00:00.000Z"), + hoursPerDay: 6, + percentage: 75, + role: "Designer", + roleId: "role_1", + dailyCostCents: 42000, + status: "PROPOSED", + metadata: {}, + createdAt: new Date("2026-03-29T00:00:00.000Z"), + updatedAt: new Date("2026-03-29T00:00:00.000Z"), + resource: { id: "resource_1", displayName: "Carol Danvers", eid: "EMP-001", lcrCents: 7000 }, + project: { id: "project_1", name: "Project One", shortCode: "PROJ-1" }, + roleEntity: { id: "role_1", name: "Designer", color: "#00AAFF" }, + demandRequirement: { + id: "demand_1", + projectId: "project_1", + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-05-15T00:00:00.000Z"), + hoursPerDay: 6, + percentage: 75, + role: "Designer", + roleId: "role_1", + headcount: 1, + status: "PROPOSED", + }, + }); + const txDemandUpdate = vi.fn().mockResolvedValue({ + id: "demand_1", + projectId: "project_1", + headcount: 1, + status: "COMPLETED", + }); + const tx = { + project: { + findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), + }, + resource: { + findUnique: vi.fn().mockResolvedValue({ + id: "resource_1", + lcrCents: 7000, + availability: { + monday: 8, + tuesday: 8, + wednesday: 8, + thursday: 8, + friday: 8, + saturday: 0, + sunday: 0, + }, + }), + }, + demandRequirement: { + findUnique: vi.fn().mockResolvedValue({ + id: "demand_1", + projectId: "project_1", + }), + update: txDemandUpdate, + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + create: assignmentCreate, + }, + vacation: { + findMany: vi.fn().mockResolvedValue([]), + }, + auditLog: { + create: auditCreate, + }, + }; + const transaction = vi.fn().mockImplementation(async (callback: (inner: typeof tx) => Promise) => callback(tx)); + const demandRecord = { + id: "demand_1", + projectId: "project_1", + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-05-15T00:00:00.000Z"), + hoursPerDay: 6, + percentage: 75, + role: "Designer", + roleId: "role_1", + headcount: 1, + status: "PROPOSED", + metadata: {}, + roleEntity: { name: "Designer" }, + project: { id: "project_1", name: "Project One", shortCode: "PROJ-1" }, + }; + const demandFindUnique = vi.fn() + .mockResolvedValueOnce(demandRecord) + .mockResolvedValueOnce(demandRecord); + const resourceFindUnique = vi.fn().mockResolvedValue({ + id: "resource_1", + displayName: "Carol Danvers", + lcrCents: 7000, + availability: { + monday: 8, + tuesday: 8, + wednesday: 8, + thursday: 8, + friday: 8, + saturday: 0, + sunday: 0, + }, + }); + const ctx = createToolContext( + { + demandRequirement: { + findUnique: demandFindUnique, + }, + project: { + findUnique: vi.fn().mockResolvedValue({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + budgetCents: 100000, + }), + }, + user: { + findMany: vi.fn().mockResolvedValue([]), + }, + notification: { + findFirst: vi.fn().mockResolvedValue(null), + create: vi.fn(), + }, + resource: { + findUnique: resourceFindUnique, + findFirst: vi.fn().mockResolvedValue(null), + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + }, + $transaction: transaction, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ALLOCATIONS], + }, + ); + + const result = await executeTool( + "fill_demand", + JSON.stringify({ demandId: "demand_1", resourceId: "resource_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual(expect.objectContaining({ + success: true, + message: "Assigned Carol Danvers to Designer on Project One (PROJ-1)", + assignmentId: "assignment_1", + })); + expect(assignmentCreate).toHaveBeenCalledTimes(1); + expect(txDemandUpdate).toHaveBeenCalledWith(expect.objectContaining({ + where: { id: "demand_1" }, + data: { status: "COMPLETED" }, + })); + expect(auditCreate).toHaveBeenCalledTimes(1); + }); +});