diff --git a/packages/api/src/__tests__/assistant-tools-client-delete-errors.test.ts b/packages/api/src/__tests__/assistant-tools-client-delete-errors.test.ts new file mode 100644 index 0000000..6e8d684 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-client-delete-errors.test.ts @@ -0,0 +1,114 @@ +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, + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { countPlanningEntries } from "@capakraken/application"; +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-master-data-mutation-test-helpers.js"; + +describe("assistant client delete tool - errors", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(countPlanningEntries).mockResolvedValue({ countsByRoleId: new Map() }); + }); + + it("returns a stable error when deleting a missing client", async () => { + const ctx = createToolContext( + { + client: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "delete_client", + JSON.stringify({ id: "client_missing" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "Client not found with the given criteria.", + }), + ); + }); + + it("returns a stable error when deleting a client that still has projects", async () => { + const clientRecord = { + id: "client_1", + name: "Acme Mobility", + code: "ACM", + parentId: null, + isActive: true, + sortOrder: 3, + tags: ["auto", "priority"], + _count: { projects: 2, children: 0 }, + }; + const ctx = createToolContext( + { + client: { + findUnique: vi.fn().mockResolvedValueOnce(clientRecord).mockResolvedValueOnce(clientRecord), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "delete_client", + JSON.stringify({ id: "client_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "Client cannot be deleted while it still has projects. Deactivate it instead.", + }), + ); + }); + + it("returns a stable error when deleting a client that still has child clients", async () => { + const clientRecord = { + id: "client_1", + name: "Acme Mobility", + code: "ACM", + parentId: null, + isActive: true, + sortOrder: 3, + tags: ["auto", "priority"], + _count: { projects: 0, children: 2 }, + }; + const ctx = createToolContext( + { + client: { + findUnique: vi.fn().mockResolvedValueOnce(clientRecord).mockResolvedValueOnce(clientRecord), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "delete_client", + JSON.stringify({ id: "client_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: + "Client cannot be deleted while it still has child clients. Remove or reassign them first.", + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-client-delete-success.test.ts b/packages/api/src/__tests__/assistant-tools-client-delete-success.test.ts new file mode 100644 index 0000000..290200a --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-client-delete-success.test.ts @@ -0,0 +1,75 @@ +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, + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { countPlanningEntries } from "@capakraken/application"; +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-master-data-mutation-test-helpers.js"; + +describe("assistant client delete tool - success", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(countPlanningEntries).mockResolvedValue({ countsByRoleId: new Map() }); + }); + + it("returns the expected assistant payload for deleting a client", async () => { + const db = { + client: { + findUnique: vi.fn(async ({ where }: { where: { id?: string; code?: string } }) => { + if (where.id === "client_1") { + return { + id: "client_1", + name: "Acme Mobility", + code: "ACM", + parentId: null, + sortOrder: 3, + tags: ["auto", "priority"], + isActive: true, + _count: { projects: 0, children: 0 }, + }; + } + return null; + }), + delete: vi.fn().mockResolvedValue({ + id: "client_1", + name: "Acme Mobility", + code: "ACM", + parentId: null, + sortOrder: 3, + tags: ["auto", "priority"], + isActive: true, + _count: { projects: 0, children: 0 }, + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({ id: "audit_1" }), + }, + }; + const ctx = createToolContext(db, { userRole: SystemRole.ADMIN }); + + const deleteClientResult = await executeTool( + "delete_client", + JSON.stringify({ id: "client_1" }), + ctx, + ); + + expect(JSON.parse(deleteClientResult.content)).toEqual( + expect.objectContaining({ + success: true, + message: "Deleted client: Acme Mobility", + }), + ); + expect(db.client.delete).toHaveBeenCalledWith({ where: { id: "client_1" } }); + expect(db.auditLog.create).toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-client-mutations-create-update-errors.test.ts b/packages/api/src/__tests__/assistant-tools-client-mutations-create-update-errors.test.ts new file mode 100644 index 0000000..7cc79fa --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-client-mutations-create-update-errors.test.ts @@ -0,0 +1,102 @@ +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, + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { countPlanningEntries } from "@capakraken/application"; +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-master-data-mutation-test-helpers.js"; + +describe("assistant client create and update tools - errors", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(countPlanningEntries).mockResolvedValue({ countsByRoleId: new Map() }); + }); + + it("returns a stable error when creating a client with a duplicate code", async () => { + const ctx = createToolContext( + { + client: { + findUnique: vi.fn(async ({ + where, + }: { + where: { id?: string; code?: string }; + }) => { + if (where.code === "ACM") { + return { id: "client_existing", code: "ACM", name: "Existing Client" }; + } + return null; + }), + }, + }, + { userRole: SystemRole.MANAGER }, + ); + + const result = await executeTool( + "create_client", + JSON.stringify({ name: "Acme Mobility", code: "ACM" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "A client with this code already exists.", + }), + ); + }); + + it("returns a stable error when creating a client with a missing parent", async () => { + const ctx = createToolContext( + { + client: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + { userRole: SystemRole.MANAGER }, + ); + + const result = await executeTool( + "create_client", + JSON.stringify({ name: "Acme Mobility", parentId: "client_missing" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "Parent client not found with the given criteria.", + }), + ); + }); + + it("returns a stable error when updating a missing client", async () => { + const ctx = createToolContext( + { + client: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + { userRole: SystemRole.MANAGER }, + ); + + const result = await executeTool( + "update_client", + JSON.stringify({ id: "client_missing", name: "Acme Mobility" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "Client not found with the given criteria.", + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-client-mutations-create-update-success.test.ts b/packages/api/src/__tests__/assistant-tools-client-mutations-create-update-success.test.ts new file mode 100644 index 0000000..7411dcd --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-client-mutations-create-update-success.test.ts @@ -0,0 +1,181 @@ +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, + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { countPlanningEntries } from "@capakraken/application"; +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-master-data-mutation-test-helpers.js"; + +describe("assistant client create and update tools - success", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(countPlanningEntries).mockResolvedValue({ countsByRoleId: new Map() }); + }); + + it("routes create and update client mutations through their backing router", async () => { + const db = { + client: { + findUnique: vi.fn().mockImplementation(async (args?: { where?: { id?: string; code?: string } }) => { + if (args?.where?.id === "client_1") { + return { + id: "client_1", + name: "Acme", + code: "ACME", + parentId: null, + isActive: true, + sortOrder: 0, + tags: [], + }; + } + if (args?.where?.code === "ACME-NEW") { + return null; + } + return null; + }), + create: vi.fn().mockResolvedValue({ + id: "client_1", + name: "Acme", + code: "ACME", + parentId: null, + isActive: true, + sortOrder: 2, + tags: ["key"], + }), + update: vi.fn().mockResolvedValue({ + id: "client_1", + name: "Acme Updated", + code: "ACME-NEW", + parentId: null, + isActive: false, + sortOrder: 3, + tags: ["vip"], + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({ id: "audit_1" }), + }, + }; + const ctx = createToolContext(db, { userRole: SystemRole.ADMIN }); + + const createClientResult = await executeTool( + "create_client", + JSON.stringify({ name: "Acme", code: "ACME", sortOrder: 2, tags: ["key"] }), + ctx, + ); + const updateClientResult = await executeTool( + "update_client", + JSON.stringify({ + id: "client_1", + name: "Acme Updated", + code: "ACME-NEW", + sortOrder: 3, + isActive: false, + tags: ["vip"], + }), + ctx, + ); + + expect(JSON.parse(createClientResult.content)).toEqual( + expect.objectContaining({ + success: true, + clientId: "client_1", + message: "Created client: Acme", + }), + ); + expect(JSON.parse(updateClientResult.content)).toEqual( + expect.objectContaining({ + success: true, + clientId: "client_1", + message: "Updated client: Acme Updated", + }), + ); + + expect(db.client.create).toHaveBeenCalled(); + expect(db.client.update).toHaveBeenCalled(); + expect(db.auditLog.create).toHaveBeenCalled(); + }); + + it("returns the expected assistant payloads for create and update client mutations", async () => { + const db = { + client: { + findUnique: vi.fn(async ({ where }: { where: { id?: string; code?: string } }) => { + if (where.id === "client_1") { + return { + id: "client_1", + name: "Acme", + code: "AC", + sortOrder: 0, + isActive: true, + parentId: null, + tags: [], + _count: { projects: 0, children: 0 }, + }; + } + + return null; + }), + create: vi.fn().mockResolvedValue({ + id: "client_1", + name: "Acme", + code: "AC", + parentId: null, + sortOrder: 2, + tags: ["auto"], + }), + update: vi.fn().mockResolvedValue({ + id: "client_1", + name: "Acme Mobility", + code: "ACM", + parentId: null, + sortOrder: 3, + tags: ["auto", "priority"], + isActive: true, + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({ id: "audit_1" }), + }, + }; + const ctx = createToolContext(db, { userRole: SystemRole.ADMIN }); + + const createClientResult = await executeTool( + "create_client", + JSON.stringify({ name: "Acme", code: "AC", sortOrder: 2, tags: ["auto"] }), + ctx, + ); + const updateClientResult = await executeTool( + "update_client", + JSON.stringify({ + id: "client_1", + name: "Acme Mobility", + code: "ACM", + sortOrder: 3, + tags: ["auto", "priority"], + }), + ctx, + ); + + expect(JSON.parse(createClientResult.content)).toEqual( + expect.objectContaining({ + success: true, + message: "Created client: Acme", + }), + ); + expect(JSON.parse(updateClientResult.content)).toEqual( + expect.objectContaining({ + success: true, + message: "Updated client: Acme Mobility", + }), + ); + }); +});