diff --git a/packages/api/src/__tests__/assistant-tools-notification-create-errors.test.ts b/packages/api/src/__tests__/assistant-tools-notification-create-errors.test.ts new file mode 100644 index 0000000..62eaf28 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-notification-create-errors.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-notification-test-helpers.js"; + +describe("assistant notification creation tools - errors", () => { + it("returns a stable assistant error when notification dueDate is invalid", async () => { + const ctx = createToolContext({}, SystemRole.MANAGER); + + const result = await executeTool( + "create_notification", + JSON.stringify({ + userId: "user_2", + type: "TASK_CREATED", + title: "Need review", + dueDate: "not-a-datetime", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Invalid dueDate: not-a-datetime", + }); + }); + + it("returns a stable assistant error when notification recipient user is missing", async () => { + const ctx = createToolContext( + { + notification: { + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "Notification_userId_fkey" }, + }), + }, + }, + SystemRole.MANAGER, + ); + + const result = await executeTool( + "create_notification", + JSON.stringify({ + userId: "user_missing", + type: "TASK_CREATED", + title: "Need review", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Notification recipient user not found with the given criteria.", + }); + }); + + it("returns a stable assistant error when notification assignee user is missing", async () => { + const ctx = createToolContext( + { + notification: { + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "Notification_assigneeId_fkey" }, + }), + }, + }, + SystemRole.MANAGER, + ); + + const result = await executeTool( + "create_notification", + JSON.stringify({ + userId: "user_2", + assigneeId: "user_missing", + type: "TASK_CREATED", + title: "Need review", + category: "TASK", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Assignee user not found with the given criteria.", + }); + }); + + it("returns a stable assistant error when notification sender user is missing", async () => { + const ctx = createToolContext( + { + notification: { + create: vi.fn().mockRejectedValue({ + code: "P2003", + message: "Foreign key constraint failed", + meta: { field_name: "Notification_senderId_fkey" }, + }), + }, + }, + SystemRole.MANAGER, + ); + + const result = await executeTool( + "create_notification", + JSON.stringify({ + userId: "user_2", + senderId: "user_missing", + type: "TASK_CREATED", + title: "Need review", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Sender user not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-notification-create-success.test.ts b/packages/api/src/__tests__/assistant-tools-notification-create-success.test.ts new file mode 100644 index 0000000..9e51145 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-notification-create-success.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-notification-test-helpers.js"; + +describe("assistant notification creation tools - success", () => { + it("creates a notification through the notification router", async () => { + const db = { + notification: { + create: vi.fn().mockResolvedValue({ id: "notification_1", userId: "user_2" }), + findUnique: vi.fn().mockResolvedValue({ + id: "notification_1", + title: "Need review", + userId: "user_2", + category: "TASK", + }), + }, + user: { + findUnique: vi.fn().mockResolvedValue({ id: "user_2", email: "user2@example.com", name: "User Two" }), + }, + }; + const ctx = createToolContext(db, SystemRole.ADMIN); + + const result = await executeTool( + "create_notification", + JSON.stringify({ + userId: "user_2", + type: "TASK_CREATED", + title: "Need review", + category: "TASK", + taskStatus: "OPEN", + dueDate: "2026-04-02T09:30:00.000Z", + channel: "in_app", + }), + ctx, + ); + + expect(db.notification.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + userId: "user_2", + type: "TASK_CREATED", + title: "Need review", + category: "TASK", + taskStatus: "OPEN", + dueDate: new Date("2026-04-02T09:30:00.000Z"), + senderId: "user_1", + channel: "in_app", + }), + }); + expect(db.notification.findUnique).toHaveBeenCalledWith({ + where: { id: "notification_1" }, + }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + notificationId: "notification_1", + message: 'Created notification "Need review".', + }), + ); + expect(result.action).toEqual({ + type: "invalidate", + scope: ["notification"], + }); + }); + + it("defaults task-like notifications to OPEN when the caller omits taskStatus", async () => { + const db = { + notification: { + create: vi.fn().mockResolvedValue({ id: "notification_2", userId: "user_2" }), + findUnique: vi.fn().mockResolvedValue({ + id: "notification_2", + title: "Need review", + userId: "user_2", + category: "TASK", + taskStatus: "OPEN", + }), + }, + user: { + findUnique: vi.fn().mockResolvedValue({ id: "user_2", email: "user2@example.com", name: "User Two" }), + }, + }; + const ctx = createToolContext(db, SystemRole.ADMIN); + + const result = await executeTool( + "create_notification", + JSON.stringify({ + userId: "user_2", + type: "TASK_CREATED", + title: "Need review", + category: "TASK", + channel: "in_app", + }), + ctx, + ); + + expect(db.notification.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + userId: "user_2", + category: "TASK", + taskStatus: "OPEN", + }), + }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + notificationId: "notification_2", + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-notification-inbox-mutations.test.ts b/packages/api/src/__tests__/assistant-tools-notification-inbox-mutations.test.ts new file mode 100644 index 0000000..604adb6 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-notification-inbox-mutations.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext, withUserLookup } from "./assistant-tools-notification-test-helpers.js"; + +describe("assistant notification inbox mutation tools", () => { + it("marks all unread notifications as read through the notification router", async () => { + const db = { + user: { + findUnique: vi.fn().mockResolvedValue({ id: "user_1" }), + }, + notification: { + updateMany: vi.fn().mockResolvedValue({ count: 3 }), + }, + }; + const ctx = createToolContext(db, SystemRole.CONTROLLER); + + const result = await executeTool("mark_notification_read", "{}", ctx); + + expect(db.notification.updateMany).toHaveBeenCalledWith({ + where: { userId: "user_1", readAt: null }, + data: { readAt: expect.any(Date) }, + }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + success: true, + message: "All unread notifications marked as read.", + }), + ); + }); + + it("deletes notifications through the router and invalidates notification queries", async () => { + const db = withUserLookup({ + notification: { + findFirst: vi.fn().mockResolvedValue({ + id: "notif_9", + userId: "user_1", + category: "NOTIFICATION", + senderId: "user_mgr", + }), + delete: vi.fn().mockResolvedValue({ id: "notif_9" }), + }, + }); + const ctx = createToolContext(db, SystemRole.USER); + + const result = await executeTool( + "delete_notification", + JSON.stringify({ id: "notif_9" }), + ctx, + ); + + expect(db.notification.delete).toHaveBeenCalledWith({ where: { id: "notif_9" } }); + expect(result.action).toEqual({ type: "invalidate", scope: ["notification"] }); + expect(result.data).toEqual({ + success: true, + id: "notif_9", + message: "Deleted notification notif_9.", + }); + }); +});