test(api): cover assistant vacation read tools

This commit is contained in:
2026-03-31 23:55:28 +02:00
parent fd2c6b6203
commit c03436945e
3 changed files with 384 additions and 0 deletions
@@ -0,0 +1,214 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VacationType } from "@capakraken/db";
import { SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
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(),
};
});
import { executeTool } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-vacation-entitlement-test-helpers.js";
describe("assistant vacation balance tools", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("routes vacation balance through the real vacation workflow", async () => {
const vacationFindMany = vi.fn().mockImplementation(async (args?: any) => {
if (args?.where?.type === VacationType.PUBLIC_HOLIDAY) {
return [];
}
if (args?.where?.type?.in) {
return [
{
type: VacationType.ANNUAL,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-06T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
},
{
type: VacationType.ANNUAL,
startDate: new Date("2026-02-03T00:00:00.000Z"),
endDate: new Date("2026-02-03T00:00:00.000Z"),
status: "PENDING",
isHalfDay: true,
},
];
}
if (args?.where?.type === VacationType.SICK) {
return [
{
startDate: new Date("2026-03-10T00:00:00.000Z"),
endDate: new Date("2026-03-10T00:00:00.000Z"),
isHalfDay: false,
},
];
}
return [];
});
const ctx = createToolContext({
resource: {
findUnique: vi.fn().mockResolvedValue({
id: "res_1",
eid: "EMP-001",
displayName: "Alice Example",
userId: "user_1",
chapter: "Delivery",
federalState: "BY",
countryId: "country_de",
metroCityId: null,
country: { code: "DE", name: "Germany" },
metroCity: null,
}),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
systemSettings: {
findUnique: vi.fn().mockResolvedValue({ vacationDefaultDays: 30 }),
},
vacationEntitlement: {
findUnique: vi.fn().mockResolvedValue(null),
create: vi.fn().mockResolvedValue({
id: "ent_2026",
resourceId: "res_1",
year: 2026,
entitledDays: 30,
carryoverDays: 0,
usedDays: 0,
pendingDays: 0,
}),
update: vi.fn().mockImplementation(async (args?: any) => ({
id: "ent_2026",
resourceId: "res_1",
year: 2026,
entitledDays: 30,
carryoverDays: 0,
usedDays: args?.data?.usedDays ?? 0,
pendingDays: args?.data?.pendingDays ?? 0,
})),
},
vacation: {
findMany: vacationFindMany,
},
}, { userRole: SystemRole.ADMIN });
const result = await executeTool(
"get_vacation_balance",
JSON.stringify({ resourceId: "res_1", year: 2026 }),
ctx,
);
expect(JSON.parse(result.content)).toEqual(expect.objectContaining({
resource: "Alice Example",
eid: "EMP-001",
year: 2026,
entitlement: 30,
carryOver: 0,
taken: 1,
pending: 0.5,
remaining: 28.5,
sickDays: 1,
deductionSummary: {
formula: "remaining = entitlement - taken - pending",
approvedVacationCount: 1,
pendingVacationCount: 1,
approvedRequestedDays: 2,
pendingRequestedDays: 0.5,
approvedDeductedDays: 1,
pendingDeductedDays: 0.5,
excludedHolidayDates: ["2026-01-06"],
holidayBasisVariants: ["Germany / BY"],
sources: {
hasCalendarHolidays: true,
hasLegacyPublicHolidayEntries: false,
},
},
vacations: [
expect.objectContaining({
type: "ANNUAL",
status: "APPROVED",
startDate: "2026-01-05",
endDate: "2026-01-06",
isHalfDay: false,
requestedDays: 2,
deductedDays: 1,
holidayCountryCode: "DE",
holidayCountryName: "Germany",
holidayFederalState: "BY",
holidayMetroCityName: null,
holidayCalendarDates: ["2026-01-06"],
holidayLegacyPublicHolidayDates: [],
holidayDetails: [{ date: "2026-01-06", source: "CALENDAR" }],
holidayContext: {
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: null,
sources: {
hasCalendarHolidays: true,
hasLegacyPublicHolidayEntries: false,
},
},
}),
expect.objectContaining({
type: "ANNUAL",
status: "PENDING",
startDate: "2026-02-03",
endDate: "2026-02-03",
isHalfDay: true,
requestedDays: 0.5,
deductedDays: 0.5,
holidayCountryCode: "DE",
holidayCountryName: "Germany",
holidayFederalState: "BY",
holidayMetroCityName: null,
holidayCalendarDates: [],
holidayLegacyPublicHolidayDates: [],
holidayDetails: [],
holidayContext: {
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: null,
sources: {
hasCalendarHolidays: false,
hasLegacyPublicHolidayEntries: false,
},
},
}),
],
}));
});
});
@@ -0,0 +1,29 @@
import { PermissionKey, SystemRole } from "@capakraken/shared";
import type { ToolContext } from "../router/assistant-tools.js";
export function createToolContext(
db: Record<string, unknown>,
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,
};
}
@@ -0,0 +1,141 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VacationType } from "@capakraken/db";
import { SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
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(),
};
});
import { executeTool } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-planning-read-test-helpers.js";
describe("assistant planning allocation and vacation read tools", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("routes upcoming vacation reads through the vacation router path and post-filters by chapter", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-01T00:00:00.000Z"));
try {
const db = {
vacation: {
findMany: vi.fn().mockResolvedValue([
{
id: "vac_1",
resourceId: "res_1",
status: "APPROVED",
type: VacationType.ANNUAL,
startDate: new Date("2026-03-05T00:00:00.000Z"),
endDate: new Date("2026-03-06T00:00:00.000Z"),
isHalfDay: false,
halfDayPart: null,
resource: {
id: "res_1",
displayName: "Bruce Banner",
eid: "EMP-001",
lcrCents: 8_000,
chapter: "Delivery",
},
requestedBy: null,
approvedBy: null,
},
{
id: "vac_2",
resourceId: "res_2",
status: "APPROVED",
type: VacationType.ANNUAL,
startDate: new Date("2026-03-07T00:00:00.000Z"),
endDate: new Date("2026-03-07T00:00:00.000Z"),
isHalfDay: true,
halfDayPart: "MORNING",
resource: {
id: "res_2",
displayName: "Tony Stark",
eid: "EMP-002",
lcrCents: 9_000,
chapter: "Finance",
},
requestedBy: null,
approvedBy: null,
},
]),
},
};
const ctx = createToolContext(db, { userRole: SystemRole.MANAGER });
const result = await executeTool(
"list_vacations_upcoming",
JSON.stringify({ chapter: "Delivery", daysAhead: 14, limit: 10 }),
ctx,
);
expect(db.vacation.findMany).toHaveBeenCalledWith({
where: {
status: "APPROVED",
endDate: { gte: new Date("2026-03-01T00:00:00.000Z") },
startDate: { lte: new Date("2026-03-15T00:00:00.000Z") },
},
include: {
resource: {
select: {
id: true,
displayName: true,
eid: true,
lcrCents: true,
chapter: true,
},
},
requestedBy: { select: { id: true, name: true, email: true } },
approvedBy: { select: { id: true, name: true, email: true } },
},
orderBy: { startDate: "asc" },
take: 10,
});
expect(JSON.parse(result.content)).toEqual([
{
resource: "Bruce Banner",
eid: "EMP-001",
chapter: "Delivery",
type: VacationType.ANNUAL,
start: "2026-03-05",
end: "2026-03-06",
isHalfDay: false,
halfDayPart: null,
},
]);
} finally {
vi.useRealTimers();
}
});
});