test(api): cover assistant vacation approvals

This commit is contained in:
2026-04-01 00:04:27 +02:00
parent 492cfb3db0
commit 58125b284c
4 changed files with 415 additions and 0 deletions
@@ -0,0 +1,66 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
};
});
vi.mock("../lib/audit.js", () => ({
createAuditEntry: vi.fn().mockResolvedValue(undefined),
}));
import { executeTool } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-holiday-capacity-test-helpers.js";
describe("assistant pending vacation approvals tool", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("routes pending vacation approvals through the vacation router path", async () => {
const db = {
vacation: {
findMany: vi.fn().mockResolvedValue([
{
id: "vac_1",
type: "ANNUAL",
startDate: new Date("2026-07-01T00:00:00.000Z"),
endDate: new Date("2026-07-03T00:00:00.000Z"),
isHalfDay: false,
resource: { displayName: "Bruce Banner", eid: "BB-1", chapter: "CGI" },
requestedBy: { id: "user_2", name: "Manager", email: "manager@example.com" },
},
]),
},
};
const ctx = createToolContext(db, [], SystemRole.MANAGER);
const result = await executeTool(
"get_pending_vacation_approvals",
JSON.stringify({ limit: 10 }),
ctx,
);
expect(db.vacation.findMany).toHaveBeenCalledWith({
where: { status: "PENDING" },
include: {
resource: { select: expect.any(Object) },
requestedBy: { select: { id: true, name: true, email: true } },
},
orderBy: { startDate: "asc" },
});
expect(JSON.parse(result.content)).toEqual([
expect.objectContaining({
id: "vac_1",
resource: "Bruce Banner",
eid: "BB-1",
chapter: "CGI",
}),
]);
});
});
@@ -0,0 +1,115 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
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, type ToolContext } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-vacation-entitlement-test-helpers.js";
describe("assistant vacation approval error paths", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns a stable assistant error when vacation approval cannot resolve the request", async () => {
const ctx = createToolContext(
{
vacation: {
findUnique: vi.fn().mockRejectedValue(
new TRPCError({ code: "NOT_FOUND", message: "Vacation not found" }),
),
},
},
{ userRole: SystemRole.MANAGER },
);
const result = await executeTool(
"approve_vacation",
JSON.stringify({ vacationId: "vac_missing" }),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: "Vacation not found with the given criteria.",
});
});
it("returns a stable assistant error when vacation approval violates lifecycle preconditions", async () => {
const ctx = createToolContext(
{
vacation: {
findUnique: vi.fn().mockResolvedValue({
id: "vac_approved",
resource: { displayName: "Alice Example" },
}),
},
},
{ userRole: SystemRole.MANAGER },
);
const result = await executeTool(
"approve_vacation",
JSON.stringify({ vacationId: "vac_approved" }),
{
...ctx,
db: {
...ctx.db,
vacation: {
...((ctx.db as Record<string, unknown>).vacation as Record<string, unknown>),
findUnique: vi.fn()
.mockResolvedValueOnce({
id: "vac_approved",
resource: { displayName: "Alice Example" },
})
.mockResolvedValueOnce({
id: "vac_approved",
resource: { displayName: "Alice Example" },
}),
update: vi.fn().mockRejectedValue(
new TRPCError({
code: "BAD_REQUEST",
message: "Only PENDING, CANCELLED, or REJECTED vacations can be approved",
}),
),
},
} as ToolContext["db"],
},
);
expect(JSON.parse(result.content)).toEqual({
error: "Vacation cannot be approved in its current status.",
});
});
});
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VacationStatus } from "@capakraken/db";
import { SystemRole } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
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, type ToolContext } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-vacation-entitlement-test-helpers.js";
describe("assistant vacation cancellation error paths", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns a stable assistant error when vacation cancellation cannot resolve the request", async () => {
const ctx = createToolContext(
{
vacation: {
findUnique: vi.fn().mockRejectedValue(
new TRPCError({ code: "NOT_FOUND", message: "Vacation not found" }),
),
},
},
{ userRole: SystemRole.MANAGER },
);
const result = await executeTool(
"cancel_vacation",
JSON.stringify({ vacationId: "vac_missing" }),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: "Vacation not found with the given criteria.",
});
});
it("returns a stable assistant error when vacation cancellation violates lifecycle preconditions", async () => {
const ctx = createToolContext(
{
vacation: {
findUnique: vi.fn().mockResolvedValue({
id: "vac_cancelled",
requestedById: "user_1",
resource: { displayName: "Alice Example", userId: "user_1" },
}),
},
},
{ userRole: SystemRole.USER, permissions: [] },
);
const result = await executeTool(
"cancel_vacation",
JSON.stringify({ vacationId: "vac_cancelled" }),
{
...ctx,
db: {
...ctx.db,
vacation: {
...((ctx.db as Record<string, unknown>).vacation as Record<string, unknown>),
findUnique: vi.fn()
.mockResolvedValueOnce({
id: "vac_cancelled",
requestedById: "user_1",
resource: { displayName: "Alice Example", userId: "user_1" },
})
.mockResolvedValueOnce({
id: "vac_cancelled",
status: VacationStatus.CANCELLED,
resource: { displayName: "Alice Example" },
}),
update: vi.fn().mockRejectedValue(
new TRPCError({
code: "BAD_REQUEST",
message: "Already cancelled",
}),
),
},
} as ToolContext["db"],
},
);
expect(JSON.parse(result.content)).toEqual({
error: "Vacation cannot be cancelled in its current status.",
});
});
});
@@ -0,0 +1,115 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
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, type ToolContext } from "../router/assistant-tools.js";
import { createToolContext } from "./assistant-tools-vacation-entitlement-test-helpers.js";
describe("assistant vacation rejection error paths", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns a stable assistant error when vacation rejection cannot resolve the request", async () => {
const ctx = createToolContext(
{
vacation: {
findUnique: vi.fn().mockRejectedValue(
new TRPCError({ code: "NOT_FOUND", message: "Vacation not found" }),
),
},
},
{ userRole: SystemRole.MANAGER },
);
const result = await executeTool(
"reject_vacation",
JSON.stringify({ vacationId: "vac_missing", reason: "Capacity freeze" }),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: "Vacation not found with the given criteria.",
});
});
it("returns a stable assistant error when vacation rejection violates lifecycle preconditions", async () => {
const ctx = createToolContext(
{
vacation: {
findUnique: vi.fn().mockResolvedValue({
id: "vac_approved",
resource: { displayName: "Alice Example" },
}),
},
},
{ userRole: SystemRole.MANAGER },
);
const result = await executeTool(
"reject_vacation",
JSON.stringify({ vacationId: "vac_approved", reason: "Capacity freeze" }),
{
...ctx,
db: {
...ctx.db,
vacation: {
...((ctx.db as Record<string, unknown>).vacation as Record<string, unknown>),
findUnique: vi.fn()
.mockResolvedValueOnce({
id: "vac_approved",
resource: { displayName: "Alice Example" },
})
.mockResolvedValueOnce({
id: "vac_approved",
resource: { displayName: "Alice Example" },
}),
update: vi.fn().mockRejectedValue(
new TRPCError({
code: "BAD_REQUEST",
message: "Only PENDING vacations can be rejected",
}),
),
},
} as ToolContext["db"],
},
);
expect(JSON.parse(result.content)).toEqual({
error: "Vacation cannot be rejected in its current status.",
});
});
});