From 616cb8510e6654f78980c24ba42a0c7bc92c449b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 23:57:53 +0200 Subject: [PATCH] test(api): cover assistant holiday calendar reads --- ...-tools-holiday-calendar-get-errors.test.ts | 31 ++++ ...sistant-tools-holiday-calendar-get.test.ts | 97 +++++++++++++ ...stant-tools-holiday-calendars-list.test.ts | 136 ++++++++++++++++++ ...sistant-tools-holiday-read-test-helpers.ts | 45 ++++++ .../assistant-tools-holiday-test-helpers.ts | 26 ++++ 5 files changed, 335 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-calendar-get-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-calendar-get.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-calendars-list.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-read-test-helpers.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-test-helpers.ts diff --git a/packages/api/src/__tests__/assistant-tools-holiday-calendar-get-errors.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-calendar-get-errors.test.ts new file mode 100644 index 0000000..3a4fad1 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-calendar-get-errors.test.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + createToolContext, + executeTool, +} from "./assistant-tools-holiday-read-test-helpers.js"; + +describe("assistant holiday calendar get tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable error when a holiday calendar cannot be found by identifier", async () => { + const ctx = createToolContext({ + holidayCalendar: { + findUnique: vi.fn().mockResolvedValue(null), + findFirst: vi.fn().mockResolvedValue(null), + }, + }); + + const result = await executeTool( + "get_holiday_calendar", + JSON.stringify({ identifier: "Missing Calendar" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Holiday calendar not found: Missing Calendar", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-calendar-get.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-calendar-get.test.ts new file mode 100644 index 0000000..8d392bc --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-calendar-get.test.ts @@ -0,0 +1,97 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + createHolidayCalendar, + createToolContext, + executeTool, +} from "./assistant-tools-holiday-read-test-helpers.js"; + +describe("assistant holiday calendar get tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("gets a holiday calendar by identifier and exposes ordered entries", async () => { + const findUnique = vi.fn().mockResolvedValue(null); + const findFirst = vi.fn().mockResolvedValue(createHolidayCalendar()); + const ctx = createToolContext({ + holidayCalendar: { + findUnique, + findFirst, + }, + }); + + const result = await executeTool( + "get_holiday_calendar", + JSON.stringify({ identifier: "Germany National" }), + ctx, + ); + + const parsed = JSON.parse(result.content) as { + id: string; + name: string; + entries: Array<{ name: string; date: string }>; + }; + + expect(findUnique).toHaveBeenCalledWith({ + where: { id: "Germany National" }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + }); + expect(findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Germany National", mode: "insensitive" } }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + }); + expect(parsed).toMatchObject({ + id: "cal_de", + name: "Germany National", + entries: [{ name: "New Year", date: "2026-01-01" }], + }); + }); + + it("resolves a calendar by fallback name lookup", async () => { + const db = { + holidayCalendar: { + findUnique: vi.fn().mockResolvedValue(null), + findFirst: vi.fn().mockResolvedValue(createHolidayCalendar()), + }, + }; + const ctx = createToolContext(db); + + const result = await executeTool( + "get_holiday_calendar", + JSON.stringify({ identifier: "Germany National" }), + ctx, + ); + + expect(db.holidayCalendar.findUnique).toHaveBeenCalledWith({ + where: { id: "Germany National" }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + }); + expect(db.holidayCalendar.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Germany National", mode: "insensitive" } }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + }); + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + id: "cal_de", + name: "Germany National", + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-calendars-list.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-calendars-list.test.ts new file mode 100644 index 0000000..bbd5406 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-calendars-list.test.ts @@ -0,0 +1,136 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + createHolidayCalendar, + createToolContext, + executeTool, +} from "./assistant-tools-holiday-read-test-helpers.js"; + +describe("assistant holiday calendar list tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("lists holiday calendars with scope metadata and entry counts", async () => { + const findMany = vi.fn().mockResolvedValue([ + createHolidayCalendar({ + id: "cal_by", + name: "Bayern Feiertage", + scopeType: "STATE", + stateCode: "BY", + priority: 10, + country: { id: "country_de", code: "DE", name: "Deutschland" }, + _count: { entries: 2 }, + entries: [ + { + id: "entry_1", + date: new Date("2026-01-06T00:00:00.000Z"), + name: "Heilige Drei Koenige", + isRecurringAnnual: true, + source: "state", + }, + ], + }), + ]); + const ctx = createToolContext({ + holidayCalendar: { + findMany, + }, + }); + + const result = await executeTool( + "list_holiday_calendars", + JSON.stringify({ countryCode: "DE", scopeType: "STATE", includeInactive: true }), + ctx, + ); + + const parsed = JSON.parse(result.content) as { + count: number; + calendars: Array<{ + name: string; + scopeType: string; + stateCode: string | null; + entryCount: number; + country: { code: string }; + }>; + }; + + expect(parsed.count).toBe(1); + expect(parsed.calendars).toHaveLength(1); + expect(findMany).toHaveBeenCalledWith({ + where: { + country: { code: { equals: "DE", mode: "insensitive" } }, + scopeType: "STATE", + }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + _count: { select: { entries: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + orderBy: [ + { country: { name: "asc" } }, + { scopeType: "asc" }, + { priority: "desc" }, + { name: "asc" }, + ], + }); + expect(parsed.calendars[0]).toMatchObject({ + name: "Bayern Feiertage", + scopeType: "STATE", + stateCode: "BY", + entryCount: 2, + country: { code: "DE" }, + }); + }); + + it("lists active holiday calendars by default", async () => { + const db = { + holidayCalendar: { + findMany: vi.fn().mockResolvedValue([ + { + ...createHolidayCalendar(), + _count: { entries: 1 }, + }, + ]), + }, + }; + const ctx = createToolContext(db); + + const result = await executeTool( + "list_holiday_calendars", + JSON.stringify({ countryCode: "de", scopeType: "COUNTRY" }), + ctx, + ); + + expect(db.holidayCalendar.findMany).toHaveBeenCalledWith({ + where: { + isActive: true, + country: { code: { equals: "DE", mode: "insensitive" } }, + scopeType: "COUNTRY", + }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + _count: { select: { entries: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + orderBy: [ + { country: { name: "asc" } }, + { scopeType: "asc" }, + { priority: "desc" }, + { name: "asc" }, + ], + }); + expect(JSON.parse(result.content)).toEqual({ + count: 1, + calendars: [ + expect.objectContaining({ + id: "cal_de", + name: "Germany National", + entryCount: 1, + }), + ], + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-read-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-holiday-read-test-helpers.ts new file mode 100644 index 0000000..c343aae --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-read-test-helpers.ts @@ -0,0 +1,45 @@ +import { vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool as executeAssistantTool } from "../router/assistant-tools.js"; + +export { createToolContext } from "./assistant-tools-holiday-test-helpers.js"; + +export function createHolidayCalendar( + overrides: Record = {}, +): Record { + return { + id: "cal_de", + name: "Germany National", + scopeType: "COUNTRY", + stateCode: null, + isActive: true, + priority: 0, + country: { id: "country_de", code: "DE", name: "Germany" }, + metroCity: null, + entries: [ + { + id: "entry_1", + date: new Date("2026-01-01T00:00:00.000Z"), + name: "New Year", + isRecurringAnnual: true, + source: "seed", + }, + ], + ...overrides, + }; +} + +export const executeTool = executeAssistantTool; diff --git a/packages/api/src/__tests__/assistant-tools-holiday-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-holiday-test-helpers.ts new file mode 100644 index 0000000..1f93663 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-test-helpers.ts @@ -0,0 +1,26 @@ +import { SystemRole } from "@capakraken/shared"; + +import type { ToolContext } from "../router/assistant-tools.js"; + +export function createToolContext( + db: Record, + permissions: string[] = [], + userRole: SystemRole = SystemRole.ADMIN, +): ToolContext { + return { + db: db as ToolContext["db"], + userId: "user_1", + userRole, + permissions: new Set(permissions) as ToolContext["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, + }; +}