diff --git a/packages/api/src/__tests__/assistant-tools-role-delete-success.test.ts b/packages/api/src/__tests__/assistant-tools-role-delete-success.test.ts new file mode 100644 index 0000000..2b4c574 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-role-delete-success.test.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; + +import { + createRoleRecord, + createToolContext, + executeTool, + resetRoleMutationMocks, +} from "./assistant-tools-role-mutation-test-helpers.js"; + +describe("assistant role delete tool - success", () => { + beforeEach(() => { + resetRoleMutationMocks(); + }); + + it("routes deletion through the backing router and returns the expected assistant payload", async () => { + const roleRecord = createRoleRecord(); + const db = { + role: { + findUnique: vi.fn() + .mockResolvedValueOnce(roleRecord) + .mockResolvedValueOnce(roleRecord) + .mockResolvedValueOnce({ + id: "role_1", + name: "Senior CG Artist", + description: "Pipeline lead", + color: "#222222", + isActive: true, + _count: { resourceRoles: 0 }, + }), + delete: vi.fn().mockResolvedValue({ id: "role_1" }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({ id: "audit_1" }), + }, + demandRequirement: { + groupBy: vi.fn().mockResolvedValue([]), + }, + assignment: { + groupBy: vi.fn().mockResolvedValue([]), + }, + }; + const ctx = createToolContext(db, { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ROLES], + }); + + const deleteRoleResult = await executeTool( + "delete_role", + JSON.stringify({ id: "role_1" }), + ctx, + ); + + expect(JSON.parse(deleteRoleResult.content)).toEqual(expect.objectContaining({ + success: true, + message: "Deleted role: Senior CG Artist", + })); + expect(db.role.delete).toHaveBeenCalledWith({ where: { id: "role_1" } }); + expect(db.auditLog.create).toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-role-mutation-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-role-mutation-test-helpers.ts new file mode 100644 index 0000000..474b8bb --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-role-mutation-test-helpers.ts @@ -0,0 +1,37 @@ +import { vi } from "vitest"; + +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 as executeAssistantTool } from "../router/assistant-tools.js"; + +export { createToolContext } from "./assistant-tools-master-data-mutation-test-helpers.js"; + +export function resetRoleMutationMocks() { + vi.clearAllMocks(); + vi.mocked(countPlanningEntries).mockResolvedValue({ countsByRoleId: new Map() }); +} + +export function createRoleRecord(overrides: Record = {}) { + return { + id: "role_1", + name: "Senior CG Artist", + description: "Pipeline lead", + color: "#222222", + isActive: true, + _count: { resourceRoles: 0 }, + resourceRoles: [], + ...overrides, + }; +} + +export const executeTool = executeAssistantTool; diff --git a/packages/api/src/__tests__/assistant-tools-role-mutations-create-update-success.test.ts b/packages/api/src/__tests__/assistant-tools-role-mutations-create-update-success.test.ts new file mode 100644 index 0000000..29424db --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-role-mutations-create-update-success.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; + +import { + createRoleRecord, + createToolContext, + executeTool, + resetRoleMutationMocks, +} from "./assistant-tools-role-mutation-test-helpers.js"; + +describe("assistant role create and update tools - success", () => { + beforeEach(() => { + resetRoleMutationMocks(); + }); + + it("routes create and update role mutations through their backing router", async () => { + const db = { + role: { + findUnique: vi.fn().mockImplementation(async (args?: { where?: { name?: string; id?: string } }) => { + if (args?.where?.name === "Pipeline TD") return null; + if (args?.where?.name === "Pipeline Lead") return null; + if (args?.where?.id === "role_pipeline") { + return { + id: "role_pipeline", + name: "Pipeline TD", + description: "Pipeline craft", + color: "#123456", + isActive: true, + _count: { resourceRoles: 0 }, + resourceRoles: [], + }; + } + return null; + }), + create: vi.fn().mockResolvedValue({ + id: "role_pipeline", + name: "Pipeline TD", + description: "Pipeline craft", + color: "#123456", + isActive: true, + _count: { resourceRoles: 0 }, + }), + update: vi.fn().mockResolvedValue({ + id: "role_pipeline", + name: "Pipeline Lead", + description: "Lead pipeline craft", + color: "#654321", + isActive: false, + _count: { resourceRoles: 0 }, + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({ id: "audit_1" }), + }, + }; + const ctx = createToolContext(db, { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ROLES], + }); + + const createRoleResult = await executeTool( + "create_role", + JSON.stringify({ name: "Pipeline TD", description: "Pipeline craft", color: "#123456" }), + ctx, + ); + const updateRoleResult = await executeTool( + "update_role", + JSON.stringify({ + id: "role_pipeline", + name: "Pipeline Lead", + description: "Lead pipeline craft", + color: "#654321", + isActive: false, + }), + ctx, + ); + + expect(JSON.parse(createRoleResult.content)).toEqual(expect.objectContaining({ + success: true, + roleId: "role_pipeline", + message: "Created role: Pipeline TD", + })); + expect(JSON.parse(updateRoleResult.content)).toEqual(expect.objectContaining({ + success: true, + roleId: "role_pipeline", + message: "Updated role: Pipeline Lead", + })); + + expect(db.role.create).toHaveBeenCalled(); + expect(db.role.update).toHaveBeenCalled(); + expect(db.auditLog.create).toHaveBeenCalled(); + }); + + it("returns the expected assistant payloads for create and update role mutations", async () => { + const roleRecord = createRoleRecord(); + const db = { + role: { + findUnique: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ id: "role_1", name: "CG Artist", description: null, color: "#111111", isActive: true }) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(roleRecord), + create: vi.fn().mockResolvedValue({ + id: "role_1", + name: "CG Artist", + description: null, + color: "#111111", + isActive: true, + _count: { resourceRoles: 0 }, + }), + update: vi.fn().mockResolvedValue({ + id: "role_1", + name: "Senior CG Artist", + description: "Pipeline lead", + color: "#222222", + isActive: true, + _count: { resourceRoles: 0 }, + }), + }, + auditLog: { + create: vi.fn().mockResolvedValue({ id: "audit_1" }), + }, + demandRequirement: { + groupBy: vi.fn().mockResolvedValue([]), + }, + assignment: { + groupBy: vi.fn().mockResolvedValue([]), + }, + }; + const ctx = createToolContext(db, { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_ROLES], + }); + + const createRoleResult = await executeTool( + "create_role", + JSON.stringify({ name: "CG Artist", color: "#111111" }), + ctx, + ); + const updateRoleResult = await executeTool( + "update_role", + JSON.stringify({ + id: "role_1", + name: "Senior CG Artist", + description: "Pipeline lead", + color: "#222222", + }), + ctx, + ); + + expect(JSON.parse(createRoleResult.content)).toEqual(expect.objectContaining({ + success: true, + message: "Created role: CG Artist", + })); + expect(JSON.parse(updateRoleResult.content)).toEqual(expect.objectContaining({ + success: true, + message: "Updated role: Senior CG Artist", + })); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-role-mutations-errors.test.ts b/packages/api/src/__tests__/assistant-tools-role-mutations-errors.test.ts new file mode 100644 index 0000000..80f2399 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-role-mutations-errors.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; + +import { + createRoleRecord, + createToolContext, + executeTool, + resetRoleMutationMocks, +} from "./assistant-tools-role-mutation-test-helpers.js"; + +describe("assistant role mutation tools", () => { + beforeEach(() => { + resetRoleMutationMocks(); + }); + + it("returns a stable error when creating a duplicate role", async () => { + const ctx = createToolContext( + { + role: { + findUnique: vi.fn().mockResolvedValue({ id: "role_existing", name: "CG Artist" }), + }, + }, + { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_ROLES], + }, + ); + + const result = await executeTool( + "create_role", + JSON.stringify({ name: "CG Artist" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual(expect.objectContaining({ + error: "A role with this name already exists.", + })); + }); + + it("returns a stable error when updating a missing role", async () => { + const ctx = createToolContext( + { + role: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_ROLES], + }, + ); + + const result = await executeTool( + "update_role", + JSON.stringify({ id: "role_missing", name: "Senior CG Artist" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual(expect.objectContaining({ + error: "Role not found with the given criteria.", + })); + }); + + it("returns a stable error when deleting an assigned role", async () => { + const roleRecord = createRoleRecord({ _count: { resourceRoles: 1 } }); + const ctx = createToolContext( + { + role: { + findUnique: vi.fn() + .mockResolvedValueOnce(roleRecord) + .mockResolvedValueOnce(roleRecord), + }, + demandRequirement: { + groupBy: vi.fn().mockResolvedValue([]), + }, + assignment: { + groupBy: vi.fn().mockResolvedValue([]), + }, + }, + { + userRole: SystemRole.MANAGER, + permissions: [PermissionKey.MANAGE_ROLES], + }, + ); + + const result = await executeTool( + "delete_role", + JSON.stringify({ id: "role_1" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual(expect.objectContaining({ + error: "Role cannot be deleted while it is still assigned. Deactivate it instead.", + })); + }); +});