From 0b535a6a5f6a0d261a1bcba3dc49f80d80c36569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:37:45 +0200 Subject: [PATCH] test(api): cover assistant estimate clone paths --- ...istant-tools-estimate-clone-errors.test.ts | 65 +++++ ...tant-tools-estimate-creation-races.test.ts | 229 ++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-estimate-clone-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-estimate-creation-races.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-estimate-clone-errors.test.ts b/packages/api/src/__tests__/assistant-tools-estimate-clone-errors.test.ts new file mode 100644 index 0000000..3bd50e7 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-estimate-clone-errors.test.ts @@ -0,0 +1,65 @@ +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(), + createEstimateExport: vi.fn(), + createEstimatePlanningHandoff: vi.fn(), + createEstimateRevision: vi.fn(), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + getEstimateById: vi.fn(), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + submitEstimateVersion: vi.fn(), + updateEstimateDraft: vi.fn(), + }; +}); + +import { cloneEstimate } from "@capakraken/application"; + +import { executeTool } from "../router/assistant-tools.js"; +import { + createToolContext, + resetEstimateToolMocks, +} from "./assistant-tools-estimate-test-helpers.js"; + +describe("assistant estimate clone errors", () => { + beforeEach(() => { + resetEstimateToolMocks(); + }); + + it("returns stable assistant errors for clone_estimate mutations", async () => { + const cases = [ + { + payload: { sourceEstimateId: "est_missing" }, + setup: () => vi.mocked(cloneEstimate).mockRejectedValueOnce(new Error("Source estimate not found")), + expected: "Estimate not found with the given criteria.", + }, + { + payload: { sourceEstimateId: "est_empty" }, + setup: () => vi.mocked(cloneEstimate).mockRejectedValueOnce(new Error("Source estimate has no versions")), + expected: "Source estimate has no versions and cannot be cloned.", + }, + ] as const; + + for (const testCase of cases) { + testCase.setup(); + const ctx = createToolContext({}, { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_PROJECTS], + }); + + const result = await executeTool( + "clone_estimate", + JSON.stringify(testCase.payload), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ error: testCase.expected }); + } + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-estimate-creation-races.test.ts b/packages/api/src/__tests__/assistant-tools-estimate-creation-races.test.ts new file mode 100644 index 0000000..c33ffe6 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-estimate-creation-races.test.ts @@ -0,0 +1,229 @@ +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(), + createEstimateExport: vi.fn(), + createEstimatePlanningHandoff: vi.fn(), + createEstimateRevision: vi.fn(), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + getEstimateById: vi.fn(), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + submitEstimateVersion: vi.fn(), + updateEstimateDraft: vi.fn(), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { + createToolContext, + resetEstimateToolMocks, +} from "./assistant-tools-estimate-test-helpers.js"; + +describe("assistant estimate creation race tools", () => { + beforeEach(() => { + resetEstimateToolMocks(); + }); + + it("returns stable assistant errors for estimate creation races", async () => { + const missingProjectCtx = createToolContext( + { + project: { + findUnique: vi.fn() + .mockResolvedValueOnce({ id: "project_1" }) + .mockResolvedValueOnce({ + id: "project_1", + shortCode: "PROJ-1", + name: "Project One", + status: "DRAFT", + startDate: new Date("2026-01-01T00:00:00.000Z"), + endDate: new Date("2026-12-31T00:00:00.000Z"), + orderType: "TIME_MATERIAL", + allocationType: "INT", + winProbability: 100, + budgetCents: 100000, + responsiblePerson: "Peter Parker", + }), + }, + estimate: { + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "Estimate_projectId_fkey" }, + }), + }, + }, + { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_PROJECTS], + }, + ); + + const missingProjectResult = await executeTool( + "create_estimate", + JSON.stringify({ name: "Delivery Estimate", projectId: "project_1" }), + missingProjectCtx, + ); + + expect(JSON.parse(missingProjectResult.content)).toEqual({ + error: "Project not found with the given criteria.", + }); + + const missingRoleCtx = createToolContext( + { + estimate: { + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "EstimateDemandLine_roleId_fkey" }, + }), + }, + rateCardLine: { + findMany: vi.fn().mockResolvedValue([]), + }, + }, + { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_PROJECTS], + }, + ); + + const missingRoleResult = await executeTool( + "create_estimate", + JSON.stringify({ + name: "Delivery Estimate", + demandLines: [ + { + roleId: "role_1", + lineType: "LABOR", + name: "Design", + hours: 40, + costRateCents: 0, + billRateCents: 0, + currency: "EUR", + costTotalCents: 0, + priceTotalCents: 0, + monthlySpread: {}, + staffingAttributes: {}, + metadata: {}, + }, + ], + }), + missingRoleCtx, + ); + + expect(JSON.parse(missingRoleResult.content)).toEqual({ + error: "Role not found with the given criteria.", + }); + + const cases = [ + { + name: "missing resource reference", + payload: { + name: "Delivery Estimate", + demandLines: [ + { + resourceId: "resource_1", + lineType: "LABOR", + name: "Animation", + hours: 40, + costRateCents: 0, + billRateCents: 0, + currency: "EUR", + costTotalCents: 0, + priceTotalCents: 0, + monthlySpread: {}, + staffingAttributes: {}, + metadata: {}, + }, + ], + }, + rejection: { + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "EstimateDemandLine_resourceId_fkey" }, + }, + expected: "Resource not found with the given criteria.", + }, + { + name: "missing scope item reference", + payload: { + name: "Delivery Estimate", + scopeItems: [ + { + sequenceNo: 1, + scopeType: "SHOT", + name: "Shot 010", + technicalSpec: {}, + metadata: {}, + }, + ], + demandLines: [ + { + scopeItemId: "scope_item_missing", + lineType: "LABOR", + name: "Lighting", + hours: 24, + costRateCents: 0, + billRateCents: 0, + currency: "EUR", + costTotalCents: 0, + priceTotalCents: 0, + monthlySpread: {}, + staffingAttributes: {}, + metadata: {}, + }, + ], + }, + rejection: { + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "EstimateDemandLine_scopeItemId_fkey" }, + }, + expected: "Estimate scope item not found with the given criteria.", + }, + { + name: "generic referenced record race", + payload: { name: "Delivery Estimate" }, + rejection: { + code: "P2025", + message: "Record to create no longer references a valid row", + meta: { cause: "Dependent record disappeared during nested estimate create" }, + }, + expected: "One of the referenced project, role, resource, or scope items no longer exists.", + }, + ] as const; + + for (const testCase of cases) { + const ctx = createToolContext( + { + estimate: { + create: vi.fn().mockRejectedValue(testCase.rejection), + }, + rateCardLine: { + findMany: vi.fn().mockResolvedValue([]), + }, + }, + { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_PROJECTS], + }, + ); + + const result = await executeTool( + "create_estimate", + JSON.stringify(testCase.payload), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: testCase.expected, + }); + } + }); +});