diff --git a/packages/api/src/__tests__/assistant-tools-project-cover-generate.test.ts b/packages/api/src/__tests__/assistant-tools-project-cover-generate.test.ts new file mode 100644 index 0000000..7936f59 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-project-cover-generate.test.ts @@ -0,0 +1,143 @@ +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(), + 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(), + }; +}); + +vi.mock("../ai-client.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createDalleClient: vi.fn(() => ({ + images: { + generate: vi.fn().mockResolvedValue({ + data: [{ b64_json: "ZmFrZQ==" }], + }), + }, + })), + loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn) => fn()), + }; +}); + +import { + createToolContext, + executeTool, +} from "./assistant-tools-project-media-test-helpers.js"; + +describe("assistant project cover generation tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes project cover generation through the real project router path", async () => { + const projectFindUnique = vi.fn() + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + status: "ACTIVE", + orderType: "CHARGEABLE", + allocationType: "INT", + budgetCents: 0, + winProbability: 100, + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-06-30T00:00:00.000Z"), + responsiblePerson: "Peter Parker", + client: null, + utilizationCategory: null, + _count: { assignments: 0, estimates: 0 }, + }) + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + client: { name: "Wayne Enterprises" }, + }); + const projectUpdate = vi.fn().mockResolvedValue({ + id: "project_1", + coverImageUrl: "data:image/png;base64,ZmFrZQ==", + }); + const ctx = createToolContext( + { + project: { + findUnique: projectFindUnique, + update: projectUpdate, + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + }, + systemSettings: { + findUnique: vi.fn().mockResolvedValue({ + id: "singleton", + imageProvider: "dalle", + aiProvider: "openai", + azureOpenAiApiKey: "sk-test", + azureOpenAiDeployment: "gpt-4o-mini", + }), + }, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_PROJECTS], + }, + ); + + const result = await executeTool( + "generate_project_cover", + JSON.stringify({ projectId: "project_1", prompt: "Blue studio lighting" }), + ctx, + ); + + expect(projectUpdate).toHaveBeenCalledWith({ + where: { id: "project_1" }, + data: { coverImageUrl: "data:image/png;base64,ZmFrZQ==" }, + }); + expect(projectFindUnique).toHaveBeenCalledWith({ + where: { id: "project_1" }, + select: expect.objectContaining({ + id: true, + shortCode: true, + name: true, + status: true, + responsiblePerson: true, + startDate: true, + endDate: true, + }), + }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + message: 'Generated cover art for project "Project One"', + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-project-cover-remove.test.ts b/packages/api/src/__tests__/assistant-tools-project-cover-remove.test.ts new file mode 100644 index 0000000..617ec58 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-project-cover-remove.test.ts @@ -0,0 +1,133 @@ +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(), + 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(), + }; +}); + +vi.mock("../ai-client.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createDalleClient: vi.fn(() => ({ + images: { + generate: vi.fn().mockResolvedValue({ + data: [{ b64_json: "ZmFrZQ==" }], + }), + }, + })), + loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn) => fn()), + }; +}); + +import { + createToolContext, + executeTool, +} from "./assistant-tools-project-media-test-helpers.js"; + +describe("assistant project cover removal tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes project cover removal through the real project router path", async () => { + const projectFindUnique = vi.fn() + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + shortCode: "PROJ-1", + status: "ACTIVE", + orderType: "CHARGEABLE", + allocationType: "INT", + budgetCents: 0, + winProbability: 100, + startDate: new Date("2026-05-01T00:00:00.000Z"), + endDate: new Date("2026-06-30T00:00:00.000Z"), + responsiblePerson: "Peter Parker", + client: null, + utilizationCategory: null, + _count: { assignments: 0, estimates: 0 }, + }) + .mockResolvedValueOnce({ + id: "project_1", + name: "Project One", + }); + const projectUpdate = vi.fn().mockResolvedValue({ + id: "project_1", + coverImageUrl: null, + }); + const ctx = createToolContext( + { + project: { + findUnique: projectFindUnique, + update: projectUpdate, + }, + assignment: { + findMany: vi.fn().mockResolvedValue([]), + }, + }, + { + userRole: SystemRole.ADMIN, + permissions: [PermissionKey.MANAGE_PROJECTS], + }, + ); + + const result = await executeTool( + "remove_project_cover", + JSON.stringify({ projectId: "project_1" }), + ctx, + ); + + expect(projectUpdate).toHaveBeenCalledWith({ + where: { id: "project_1" }, + data: { coverImageUrl: null }, + }); + expect(projectFindUnique).toHaveBeenCalledWith({ + where: { id: "project_1" }, + select: expect.objectContaining({ + id: true, + shortCode: true, + name: true, + status: true, + responsiblePerson: true, + startDate: true, + endDate: true, + }), + }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + message: 'Removed cover art from project "Project One"', + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-project-media-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-project-media-test-helpers.ts new file mode 100644 index 0000000..c7e9745 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-project-media-test-helpers.ts @@ -0,0 +1,34 @@ +import { PermissionKey, SystemRole } from "@capakraken/shared"; + +import { + executeTool as executeAssistantTool, + 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, + }; +} + +export const executeTool = executeAssistantTool;