diff --git a/packages/api/src/__tests__/assistant-tools-dispo-import-batch-delegation.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-import-batch-delegation.test.ts new file mode 100644 index 0000000..56b3b6a --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-import-batch-delegation.test.ts @@ -0,0 +1,149 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ImportBatchStatus } from "@capakraken/db"; +import { 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 { + assessDispoImportReadiness, + commitDispoImportBatch, + stageDispoImportBatch, +} from "@capakraken/application"; +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-dispo-test-helpers.js"; + +describe("assistant dispo import batch delegation tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("delegates dispo import batch staging through the real router path", async () => { + vi.mocked(stageDispoImportBatch).mockResolvedValue({ + id: "batch_1", + status: ImportBatchStatus.STAGED, + } as never); + const ctx = createToolContext({}, { userRole: SystemRole.ADMIN }); + + const result = await executeTool( + "stage_dispo_import_batch", + JSON.stringify({ + chargeabilityWorkbookPath: "/imports/chargeability.xlsx", + planningWorkbookPath: "/imports/planning.xlsx", + referenceWorkbookPath: "/imports/reference.xlsx", + costWorkbookPath: "/imports/cost.xlsx", + rosterWorkbookPath: "/imports/roster.xlsx", + notes: "March import", + }), + ctx, + ); + + expect(stageDispoImportBatch).toHaveBeenCalledWith(ctx.db, { + chargeabilityWorkbookPath: "/imports/chargeability.xlsx", + planningWorkbookPath: "/imports/planning.xlsx", + referenceWorkbookPath: "/imports/reference.xlsx", + costWorkbookPath: "/imports/cost.xlsx", + rosterWorkbookPath: "/imports/roster.xlsx", + notes: "March import", + }); + expect(JSON.parse(result.content)).toEqual({ + id: "batch_1", + status: ImportBatchStatus.STAGED, + }); + }); + + it("delegates dispo import batch validation through the real router path", async () => { + vi.mocked(assessDispoImportReadiness).mockResolvedValue({ + ready: false, + checks: [{ key: "stagedResources", status: "warning" }], + } as never); + const ctx = createToolContext({}, { userRole: SystemRole.ADMIN }); + + const result = await executeTool( + "validate_dispo_import_batch", + JSON.stringify({ + chargeabilityWorkbookPath: "/imports/chargeability.xlsx", + planningWorkbookPath: "/imports/planning.xlsx", + referenceWorkbookPath: "/imports/reference.xlsx", + importBatchId: "batch_1", + }), + ctx, + ); + + expect(assessDispoImportReadiness).toHaveBeenCalledWith({ + chargeabilityWorkbookPath: "/imports/chargeability.xlsx", + planningWorkbookPath: "/imports/planning.xlsx", + referenceWorkbookPath: "/imports/reference.xlsx", + importBatchId: "batch_1", + }); + expect(JSON.parse(result.content)).toEqual({ + ready: false, + checks: [{ key: "stagedResources", status: "warning" }], + }); + }); + + it("delegates dispo import batch commits through the real router path", async () => { + vi.mocked(commitDispoImportBatch).mockResolvedValue({ + importedAssignments: 7, + importedProjects: 3, + } as never); + const ctx = createToolContext( + { + auditLog: { + create: vi.fn().mockResolvedValue(undefined), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "commit_dispo_import_batch", + JSON.stringify({ + importBatchId: "batch_1", + allowTbdUnresolved: true, + importTbdProjects: false, + }), + ctx, + ); + + expect(commitDispoImportBatch).toHaveBeenCalledWith(ctx.db, { + importBatchId: "batch_1", + allowTbdUnresolved: true, + importTbdProjects: false, + }); + expect(JSON.parse(result.content)).toEqual({ + importedAssignments: 7, + importedProjects: 3, + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-dispo-import-batch-list-cancel.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-import-batch-list-cancel.test.ts new file mode 100644 index 0000000..c8fe2bf --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-import-batch-list-cancel.test.ts @@ -0,0 +1,131 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ImportBatchStatus } from "@capakraken/db"; +import { 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-dispo-test-helpers.js"; + +describe("assistant dispo import batch list and cancel tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("lists dispo import batches through the real dispo router path", async () => { + const findMany = vi.fn().mockResolvedValue([ + { + id: "batch_2", + status: ImportBatchStatus.STAGED, + createdAt: new Date("2026-03-30T10:00:00.000Z"), + }, + { + id: "batch_1", + status: ImportBatchStatus.STAGED, + createdAt: new Date("2026-03-30T09:00:00.000Z"), + }, + ]); + const ctx = createToolContext( + { + importBatch: { + findMany, + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "list_dispo_import_batches", + JSON.stringify({ status: ImportBatchStatus.STAGED, limit: 1 }), + ctx, + ); + + expect(findMany).toHaveBeenCalledWith({ + where: { status: ImportBatchStatus.STAGED }, + orderBy: { createdAt: "desc" }, + take: 2, + }); + expect(JSON.parse(result.content)).toEqual({ + items: [ + expect.objectContaining({ + id: "batch_2", + status: ImportBatchStatus.STAGED, + }), + ], + nextCursor: "batch_1", + }); + }); + + it("cancels a dispo import batch through the real router path", async () => { + const findUnique = vi.fn().mockResolvedValue({ + id: "batch_1", + status: ImportBatchStatus.STAGED, + }); + const update = vi.fn().mockResolvedValue({ + id: "batch_1", + status: ImportBatchStatus.CANCELLED, + }); + const ctx = createToolContext( + { + importBatch: { + findUnique, + update, + }, + auditLog: { + create: vi.fn().mockResolvedValue(undefined), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "cancel_dispo_import_batch", + JSON.stringify({ id: "batch_1" }), + ctx, + ); + + expect(findUnique).toHaveBeenCalledWith({ + where: { id: "batch_1" }, + select: { id: true, status: true }, + }); + expect(update).toHaveBeenCalledWith({ + where: { id: "batch_1" }, + data: { status: ImportBatchStatus.CANCELLED }, + }); + expect(JSON.parse(result.content)).toEqual({ + id: "batch_1", + status: ImportBatchStatus.CANCELLED, + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-dispo-import.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-import.test.ts new file mode 100644 index 0000000..176faec --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-import.test.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; +import { + createToolContext, + executeTool, + resetAssistantImportToolTestState, +} from "./assistant-tools-import-dispo-webhooks-test-helpers.js"; + +describe("assistant dispo import tools", () => { + beforeEach(async () => { + await resetAssistantImportToolTestState(); + }); + + it("enforces admin access for dispo batch inspection via the backing router", async () => { + const ctx = createToolContext( + { + importBatch: { + findUnique: vi.fn(), + }, + }, + { userRole: SystemRole.USER }, + ); + + const result = await executeTool( + "get_dispo_import_batch", + JSON.stringify({ id: "batch_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "You do not have permission to perform this action.", + }), + ); + }); + + it("returns a stable assistant error for a missing dispo import batch", async () => { + const ctx = createToolContext( + { + importBatch: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "get_dispo_import_batch", + JSON.stringify({ id: "batch_missing" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Import batch not found with the given criteria.", + }); + }); +});