From 542d61bed3c795b78ae6701874b2e9948f583d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:36:17 +0200 Subject: [PATCH] test(api): cover assistant dispo staged reads --- ...ools-dispo-staged-assignments-read.test.ts | 94 ++++++++++++ ...staged-listings-resources-projects.test.ts | 138 ++++++++++++++++++ ...tools-dispo-staged-unresolved-read.test.ts | 90 ++++++++++++ ...-tools-dispo-staged-vacations-read.test.ts | 90 ++++++++++++ .../assistant-tools-dispo-test-helpers.ts | 28 ++++ 5 files changed, 440 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-dispo-staged-assignments-read.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-dispo-staged-listings-resources-projects.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-dispo-staged-unresolved-read.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-dispo-staged-vacations-read.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-dispo-test-helpers.ts diff --git a/packages/api/src/__tests__/assistant-tools-dispo-staged-assignments-read.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-staged-assignments-read.test.ts new file mode 100644 index 0000000..5453e8e --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-staged-assignments-read.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { StagedRecordStatus } 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 staged assignment listing tool", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("lists staged dispo assignments through the real router path", async () => { + const findMany = vi.fn().mockResolvedValue([ + { + id: "sa_1", + importBatchId: "batch_1", + resourceExternalId: "EMP-001", + status: StagedRecordStatus.PARSED, + }, + ]); + const ctx = createToolContext( + { + stagedAssignment: { + findMany, + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "list_dispo_staged_assignments", + JSON.stringify({ + importBatchId: "batch_1", + resourceExternalId: "EMP-001", + status: StagedRecordStatus.PARSED, + limit: 10, + }), + ctx, + ); + + expect(findMany).toHaveBeenCalledWith({ + where: { + importBatchId: "batch_1", + status: StagedRecordStatus.PARSED, + resourceExternalId: "EMP-001", + }, + orderBy: { createdAt: "asc" }, + take: 11, + }); + expect(JSON.parse(result.content)).toEqual({ + items: [ + expect.objectContaining({ + id: "sa_1", + resourceExternalId: "EMP-001", + status: StagedRecordStatus.PARSED, + }), + ], + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-dispo-staged-listings-resources-projects.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-staged-listings-resources-projects.test.ts new file mode 100644 index 0000000..da470bb --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-staged-listings-resources-projects.test.ts @@ -0,0 +1,138 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { StagedRecordStatus } 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 staged resource and project listing tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("forwards staged dispo resource queries through the real dispo router path", async () => { + const ctx = createToolContext( + { + stagedResource: { + findMany: vi.fn().mockResolvedValue([ + { + id: "sr_1", + importBatchId: "batch_1", + canonicalExternalId: "EMP-001", + status: "PARSED", + }, + ]), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "list_dispo_staged_resources", + JSON.stringify({ importBatchId: "batch_1", status: "PARSED", limit: 10 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + items: [ + expect.objectContaining({ + id: "sr_1", + importBatchId: "batch_1", + canonicalExternalId: "EMP-001", + status: "PARSED", + }), + ], + }); + }); + + it("lists staged dispo projects through the real router path", async () => { + const findMany = vi.fn().mockResolvedValue([ + { + id: "sp_2", + importBatchId: "batch_1", + projectKey: "BETA", + status: StagedRecordStatus.PARSED, + isTbd: true, + }, + { + id: "sp_1", + importBatchId: "batch_1", + projectKey: "OMEGA", + status: StagedRecordStatus.PARSED, + isTbd: true, + }, + ]); + const ctx = createToolContext( + { + stagedProject: { + findMany, + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "list_dispo_staged_projects", + JSON.stringify({ + importBatchId: "batch_1", + status: StagedRecordStatus.PARSED, + isTbd: true, + limit: 1, + }), + ctx, + ); + + expect(findMany).toHaveBeenCalledWith({ + where: { + importBatchId: "batch_1", + status: StagedRecordStatus.PARSED, + isTbd: true, + }, + orderBy: { projectKey: "asc" }, + take: 2, + }); + expect(JSON.parse(result.content)).toEqual({ + items: [ + expect.objectContaining({ + id: "sp_2", + projectKey: "BETA", + isTbd: true, + }), + ], + nextCursor: "sp_1", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-dispo-staged-unresolved-read.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-staged-unresolved-read.test.ts new file mode 100644 index 0000000..b648ddc --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-staged-unresolved-read.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DispoStagedRecordType } 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 staged unresolved listing tool", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("lists staged dispo unresolved records through the real router path", async () => { + const findMany = vi.fn().mockResolvedValue([ + { + id: "sur_1", + importBatchId: "batch_1", + recordType: DispoStagedRecordType.UNRESOLVED, + }, + ]); + const ctx = createToolContext( + { + stagedUnresolvedRecord: { + findMany, + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "list_dispo_staged_unresolved_records", + JSON.stringify({ + importBatchId: "batch_1", + recordType: DispoStagedRecordType.UNRESOLVED, + limit: 10, + }), + ctx, + ); + + expect(findMany).toHaveBeenCalledWith({ + where: { + importBatchId: "batch_1", + recordType: DispoStagedRecordType.UNRESOLVED, + }, + orderBy: { createdAt: "asc" }, + take: 11, + }); + expect(JSON.parse(result.content)).toEqual({ + items: [ + expect.objectContaining({ + id: "sur_1", + recordType: DispoStagedRecordType.UNRESOLVED, + }), + ], + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-dispo-staged-vacations-read.test.ts b/packages/api/src/__tests__/assistant-tools-dispo-staged-vacations-read.test.ts new file mode 100644 index 0000000..ea3954a --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-staged-vacations-read.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +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 staged vacation listing tool", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("lists staged dispo vacations through the real router path", async () => { + const findMany = vi.fn().mockResolvedValue([ + { + id: "sv_1", + importBatchId: "batch_1", + resourceExternalId: "EMP-001", + startDate: new Date("2026-04-01T00:00:00.000Z"), + }, + ]); + const ctx = createToolContext( + { + stagedVacation: { + findMany, + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "list_dispo_staged_vacations", + JSON.stringify({ + importBatchId: "batch_1", + resourceExternalId: "EMP-001", + limit: 10, + }), + ctx, + ); + + expect(findMany).toHaveBeenCalledWith({ + where: { + importBatchId: "batch_1", + resourceExternalId: "EMP-001", + }, + orderBy: { startDate: "asc" }, + take: 11, + }); + expect(JSON.parse(result.content)).toEqual({ + items: [ + expect.objectContaining({ + id: "sv_1", + resourceExternalId: "EMP-001", + }), + ], + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-dispo-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-dispo-test-helpers.ts new file mode 100644 index 0000000..20c735c --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-dispo-test-helpers.ts @@ -0,0 +1,28 @@ +import { SystemRole, type PermissionKey } from "@capakraken/shared"; +import type { ToolContext } from "../router/assistant-tools.js"; + +export function createToolContext( + db: Record, + options?: { + permissions?: PermissionKey[]; + userRole?: SystemRole; + }, +): ToolContext { + const userRole = options?.userRole ?? SystemRole.ADMIN; + return { + db: db as ToolContext["db"], + userId: "user_1", + userRole, + permissions: new Set(options?.permissions ?? []), + session: { + user: { email: "assistant@example.com", name: "Assistant User", image: null }, + expires: "2026-03-29T00:00:00.000Z", + }, + dbUser: { + id: "user_1", + systemRole: userRole, + permissionOverrides: null, + }, + roleDefaults: null, + }; +}