Files
CapaKraken/packages/api/src/__tests__/assistant-tools-advanced.test.ts
T

2146 lines
62 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { AllocationStatus, PermissionKey, 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,
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
};
});
vi.mock("../sse/event-bus.js", () => ({
emitAllocationCreated: vi.fn(),
emitAllocationDeleted: vi.fn(),
emitAllocationUpdated: vi.fn(),
emitProjectShifted: vi.fn(),
}));
vi.mock("../lib/budget-alerts.js", () => ({
checkBudgetThresholds: vi.fn(),
}));
vi.mock("../lib/cache.js", () => ({
invalidateDashboardCache: vi.fn(),
}));
import { executeTool, type ToolContext } from "../router/assistant-tools.js";
function createToolContext(
db: Record<string, unknown>,
permissions: PermissionKey[] = [],
userRole: SystemRole = SystemRole.ADMIN,
): ToolContext {
return {
db: db as ToolContext["db"],
userId: "user_1",
userRole,
permissions: new Set(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,
};
}
describe("assistant advanced tools and scoping", () => {
it("finds the best project resource with holiday-aware remaining capacity and LCR ranking", async () => {
const assignmentFindMany = vi
.fn()
.mockResolvedValueOnce([
{
resourceId: "res_carol",
hoursPerDay: 2,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
status: "PROPOSED",
resource: {
id: "res_carol",
eid: "carol.danvers",
displayName: "Carol Danvers",
chapter: "Delivery",
lcrCents: 7664,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
countryId: "country_de",
federalState: "HH",
metroCityId: "city_hamburg",
country: { code: "DE", name: "Deutschland" },
metroCity: { name: "Hamburg" },
areaRole: { name: "Artist" },
},
},
{
resourceId: "res_steve",
hoursPerDay: 4,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
status: "CONFIRMED",
resource: {
id: "res_steve",
eid: "steve.rogers",
displayName: "Steve Rogers",
chapter: "Delivery",
lcrCents: 13377,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
countryId: "country_de",
federalState: "BY",
metroCityId: "city_augsburg",
country: { code: "DE", name: "Deutschland" },
metroCity: { name: "Augsburg" },
areaRole: { name: "Artist" },
},
},
])
.mockResolvedValueOnce([
{
resourceId: "res_carol",
projectId: "project_lari",
hoursPerDay: 2,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
status: "PROPOSED",
project: { name: "Gelddruckmaschine", shortCode: "LARI" },
},
{
resourceId: "res_steve",
projectId: "project_lari",
hoursPerDay: 4,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
status: "CONFIRMED",
project: { name: "Gelddruckmaschine", shortCode: "LARI" },
},
]);
const ctx = createToolContext(
{
project: {
findUnique: vi
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: "project_lari",
name: "Gelddruckmaschine",
shortCode: "LARI",
status: "ACTIVE",
responsiblePerson: "Larissa Joos",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
})
.mockResolvedValueOnce({
id: "project_lari",
name: "Gelddruckmaschine",
shortCode: "LARI",
status: "ACTIVE",
responsiblePerson: "Larissa Joos",
}),
findFirst: vi.fn(),
},
assignment: {
findMany: assignmentFindMany,
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
},
[PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"find_best_project_resource",
JSON.stringify({
projectIdentifier: "LARI",
startDate: "2026-01-05",
endDate: "2026-01-16",
minHoursPerDay: 3,
rankingMode: "lowest_lcr",
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
project: { shortCode: string };
candidateCount: number;
bestMatch: {
name: string;
remainingHoursPerDay: number;
lcrCents: number | null;
federalState: string | null;
metroCity: string | null;
baseAvailableHours: number;
holidaySummary: { count: number };
};
candidates: Array<{
name: string;
remainingHoursPerDay: number;
workingDays: number;
baseAvailableHours: number;
holidaySummary: { count: number; hoursDeduction: number };
capacityBreakdown: { holidayHoursDeduction: number };
}>;
};
expect(parsed.project.shortCode).toBe("LARI");
expect(parsed.candidateCount).toBe(2);
expect(parsed.bestMatch).toEqual(
expect.objectContaining({
name: "Carol Danvers",
remainingHoursPerDay: 6,
lcrCents: 7664,
federalState: "HH",
metroCity: "Hamburg",
baseAvailableHours: 80,
holidaySummary: expect.objectContaining({ count: 0 }),
}),
);
expect(parsed.candidates).toEqual([
expect.objectContaining({
name: "Carol Danvers",
remainingHoursPerDay: 6,
workingDays: 10,
baseAvailableHours: 80,
holidaySummary: expect.objectContaining({ count: 0, hoursDeduction: 0 }),
capacityBreakdown: expect.objectContaining({ holidayHoursDeduction: 0 }),
}),
expect.objectContaining({
name: "Steve Rogers",
remainingHoursPerDay: 4,
workingDays: 9,
baseAvailableHours: 80,
holidaySummary: expect.objectContaining({ count: 1, hoursDeduction: 8 }),
capacityBreakdown: expect.objectContaining({ holidayHoursDeduction: 8 }),
}),
]);
});
it("requires the dedicated advanced assistant permission for the high-level resource tool", async () => {
const ctx = createToolContext({}, [PermissionKey.VIEW_COSTS]);
const result = await executeTool(
"find_best_project_resource",
JSON.stringify({ projectIdentifier: "LARI" }),
ctx,
);
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
error: expect.stringContaining(PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS),
}),
);
});
it("returns project shift preview details from the canonical timeline router", async () => {
const projectFindUnique = vi.fn().mockImplementation((args: { where?: { id?: string; shortCode?: string }; select?: Record<string, unknown> }) => {
if (args.where?.id === "GDM") {
return Promise.resolve(null);
}
if (args.where?.shortCode === "GDM") {
return Promise.resolve({
id: "project_shift",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: "Larissa",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
});
}
if (args.select && "budgetCents" in args.select) {
return Promise.resolve({
id: "project_shift",
budgetCents: 100000,
winProbability: 100,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
});
}
return Promise.resolve({
id: "project_shift",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: "Larissa",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
});
});
const ctx = createToolContext(
{
project: {
findUnique: projectFindUnique,
findFirst: vi.fn(),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
},
[PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"preview_project_shift",
JSON.stringify({
projectIdentifier: "GDM",
newStartDate: "2026-01-19",
newEndDate: "2026-01-30",
}),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
project: {
id: "project_shift",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: "Larissa",
startDate: "2026-01-05",
endDate: "2026-01-16",
},
requestedShift: {
newStartDate: "2026-01-19",
newEndDate: "2026-01-30",
},
preview: {
valid: true,
errors: [],
warnings: [],
conflictDetails: [],
costImpact: {
currentTotalCents: 0,
newTotalCents: 0,
deltaCents: 0,
budgetCents: 100000,
budgetUtilizationBefore: 0,
budgetUtilizationAfter: 0,
wouldExceedBudget: false,
},
},
});
});
it("returns timeline entries view with demand, assignment, and holiday overlay context", async () => {
const ctx = createToolContext(
{
demandRequirement: {
findMany: vi.fn().mockResolvedValue([
{
id: "dem_1",
projectId: "project_1",
resourceId: null,
role: "Artist",
hoursPerDay: 8,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-09T00:00:00.000Z"),
status: "OPEN",
metadata: null,
project: {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
orderType: "CHARGEABLE",
clientId: "client_1",
budgetCents: 0,
winProbability: 100,
status: "ACTIVE",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
staffingReqs: null,
responsiblePerson: "Larissa",
color: "#fff",
},
roleEntity: null,
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
id: "asg_by",
projectId: "project_1",
resourceId: "res_by",
role: "Artist",
hoursPerDay: 8,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-09T00:00:00.000Z"),
status: "CONFIRMED",
metadata: null,
resource: {
id: "res_by",
displayName: "Bayern User",
eid: "EMP-BY",
chapter: "Delivery",
lcrCents: 10000,
},
project: {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
orderType: "CHARGEABLE",
clientId: "client_1",
budgetCents: 0,
winProbability: 100,
status: "ACTIVE",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
staffingReqs: null,
responsiblePerson: "Larissa",
color: "#fff",
},
roleEntity: null,
},
{
id: "asg_hh",
projectId: "project_1",
resourceId: "res_hh",
role: "Artist",
hoursPerDay: 8,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-09T00:00:00.000Z"),
status: "CONFIRMED",
metadata: null,
resource: {
id: "res_hh",
displayName: "Hamburg User",
eid: "EMP-HH",
chapter: "Delivery",
lcrCents: 10000,
},
project: {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
orderType: "CHARGEABLE",
clientId: "client_1",
budgetCents: 0,
winProbability: 100,
status: "ACTIVE",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
staffingReqs: null,
responsiblePerson: "Larissa",
color: "#fff",
},
roleEntity: null,
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_by",
countryId: "country_de",
federalState: "BY",
metroCityId: "city_munich",
country: { code: "DE" },
metroCity: { name: "Muenchen" },
},
{
id: "res_hh",
countryId: "country_de",
federalState: "HH",
metroCityId: "city_hamburg",
country: { code: "DE" },
metroCity: { name: "Hamburg" },
},
]),
},
project: {
findMany: vi.fn(),
},
},
[PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"get_timeline_entries_view",
JSON.stringify({
startDate: "2026-01-05",
endDate: "2026-01-09",
projectIds: ["project_1"],
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
summary: {
demandCount: number;
assignmentCount: number;
overlayCount: number;
resourceCount: number;
};
demands: Array<{ id: string }>;
assignments: Array<{ id: string }>;
holidayOverlays: Array<{ resourceId: string; startDate: string; note: string; scope: string }>;
};
expect(parsed.summary).toEqual(
expect.objectContaining({
demandCount: 1,
assignmentCount: 2,
overlayCount: 1,
resourceCount: 2,
}),
);
expect(parsed.demands).toHaveLength(1);
expect(parsed.assignments).toHaveLength(2);
expect(parsed.holidayOverlays).toEqual([
expect.objectContaining({
resourceId: "res_by",
startDate: "2026-01-06",
note: "Heilige Drei Könige",
scope: "STATE",
}),
]);
});
it("returns project timeline context with cross-project overlap summaries", async () => {
const project = {
id: "project_ctx",
name: "Gelddruckmaschine",
shortCode: "GDM",
orderType: "CHARGEABLE",
budgetCents: 100000,
winProbability: 100,
status: "ACTIVE",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
staffingReqs: null,
};
const { listAssignmentBookings } = await import("@capakraken/application");
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "asg_project",
projectId: "project_ctx",
resourceId: "res_1",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
hoursPerDay: 6,
dailyCostCents: 0,
status: "CONFIRMED",
project: { id: "project_ctx", name: "Gelddruckmaschine", shortCode: "GDM", status: "ACTIVE", orderType: "CHARGEABLE", dynamicFields: null },
resource: { id: "res_1", displayName: "Alice", chapter: "Delivery" },
},
{
id: "asg_other",
projectId: "project_other",
resourceId: "res_1",
startDate: new Date("2026-01-08T00:00:00.000Z"),
endDate: new Date("2026-01-10T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
status: "CONFIRMED",
project: { id: "project_other", name: "Other Project", shortCode: "OTH", status: "ACTIVE", orderType: "CHARGEABLE", dynamicFields: null },
resource: { id: "res_1", displayName: "Alice", chapter: "Delivery" },
},
]);
const ctx = createToolContext(
{
project: {
findUnique: vi
.fn()
.mockResolvedValueOnce(project)
.mockResolvedValueOnce(project),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([
{
id: "dem_ctx",
projectId: "project_ctx",
resourceId: null,
role: "Artist",
hoursPerDay: 6,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
status: "OPEN",
metadata: null,
project,
roleEntity: null,
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
id: "asg_project",
projectId: "project_ctx",
resourceId: "res_1",
role: "Artist",
hoursPerDay: 6,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
status: "CONFIRMED",
metadata: null,
resource: {
id: "res_1",
displayName: "Alice",
eid: "EMP-1",
chapter: "Delivery",
lcrCents: 10000,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
},
project,
roleEntity: null,
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
countryId: "country_de",
federalState: "BY",
metroCityId: "city_munich",
country: { code: "DE" },
metroCity: { name: "Muenchen" },
},
]),
},
},
[PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"get_project_timeline_context",
JSON.stringify({
projectIdentifier: "project_ctx",
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
project: { id: string; shortCode: string };
summary: {
demandCount: number;
assignmentCount: number;
conflictedAssignmentCount: number;
overlayCount: number;
};
assignmentConflicts: Array<{
assignmentId: string;
crossProjectOverlapCount: number;
overlaps: Array<{ projectShortCode: string; sameProject: boolean }>;
}>;
holidayOverlays: Array<{ startDate: string }>;
};
expect(parsed.project).toEqual(
expect.objectContaining({
id: "project_ctx",
shortCode: "GDM",
}),
);
expect(parsed.summary).toEqual(
expect.objectContaining({
demandCount: 1,
assignmentCount: 1,
conflictedAssignmentCount: 1,
overlayCount: 1,
}),
);
expect(parsed.assignmentConflicts).toEqual([
expect.objectContaining({
assignmentId: "asg_project",
crossProjectOverlapCount: 1,
overlaps: expect.arrayContaining([
expect.objectContaining({
projectShortCode: "OTH",
sameProject: false,
}),
]),
}),
]);
expect(parsed.holidayOverlays).toEqual([
expect.objectContaining({
startDate: "2026-01-06",
}),
]);
});
it("updates timeline allocations inline through the real timeline router mutation", async () => {
const existingAssignment = {
id: "assignment_1",
demandRequirementId: null,
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
hoursPerDay: 4,
percentage: 50,
role: "Compositor",
roleId: "role_comp",
dailyCostCents: 20000,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
resource: {
id: "resource_1",
displayName: "Alice",
eid: "E-001",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
},
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
demandRequirement: null,
};
const updatedAssignment = {
...existingAssignment,
hoursPerDay: 6,
endDate: new Date("2026-03-21"),
percentage: 75,
dailyCostCents: 30000,
metadata: { includeSaturday: true },
updatedAt: new Date("2026-03-14"),
};
const db = {
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(existingAssignment),
update: vi.fn().mockResolvedValue(updatedAssignment),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
id: "resource_1",
eid: "E-001",
displayName: "Alice",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
}),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"update_timeline_allocation_inline",
JSON.stringify({
allocationId: "assignment_1",
hoursPerDay: 6,
endDate: "2026-03-21",
includeSaturday: true,
}),
ctx,
);
expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] });
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
success: true,
allocation: expect.objectContaining({
id: "assignment_1",
hoursPerDay: 6,
endDate: "2026-03-21",
}),
}),
);
expect(db.assignment.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: "assignment_1" },
}),
);
});
it("quick-assigns a timeline resource through the real timeline router mutation", async () => {
const createdAssignment = {
id: "assignment_quick_1",
demandRequirementId: null,
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
hoursPerDay: 8,
percentage: 100,
role: "Team Member",
roleId: null,
dailyCostCents: 40000,
status: AllocationStatus.PROPOSED,
metadata: { source: "quickAssign" },
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
resource: {
id: "resource_1",
displayName: "Alice",
eid: "E-001",
lcrCents: 5000,
},
project: { id: "project_1", name: "Gelddruckmaschine", shortCode: "GDM" },
roleEntity: null,
demandRequirement: null,
};
const db = {
project: {
findUnique: vi.fn().mockResolvedValue({
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
}),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
id: "resource_1",
eid: "E-001",
displayName: "Alice",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
}),
},
allocation: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn(),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue(createdAssignment),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"quick_assign_timeline_resource",
JSON.stringify({
resourceIdentifier: "resource_1",
projectIdentifier: "project_1",
startDate: "2026-03-16",
endDate: "2026-03-20",
hoursPerDay: 8,
}),
ctx,
);
expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] });
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
success: true,
allocation: expect.objectContaining({
id: "assignment_quick_1",
projectId: "project_1",
resourceId: "resource_1",
hoursPerDay: 8,
}),
}),
);
expect(db.assignment.create).toHaveBeenCalled();
});
it("returns a stable conflict error for quick-assign timeline mutations", async () => {
const db = {
project: {
findUnique: vi.fn().mockResolvedValue({
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
}),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
id: "resource_1",
eid: "E-001",
displayName: "Alice",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
}),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockRejectedValue(
new TRPCError({
code: "CONFLICT",
message: "Resource is already assigned to this project with overlapping dates",
}),
),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"quick_assign_timeline_resource",
JSON.stringify({
resourceIdentifier: "resource_1",
projectIdentifier: "project_1",
startDate: "2026-03-16",
endDate: "2026-03-20",
hoursPerDay: 8,
}),
ctx,
);
expect(result.action).toBeUndefined();
expect(JSON.parse(result.content)).toEqual({
error: "Resource is already assigned to this project with overlapping dates",
});
});
it("returns stable not-found errors when quick-assign timeline targets disappear mid-mutation", async () => {
const baseProject = {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
};
const baseResource = {
id: "resource_1",
eid: "E-001",
displayName: "Alice",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
};
const cases = [
{
name: "missing project",
tx: {
project: {
findUnique: vi.fn().mockResolvedValue(null),
},
resource: {
findUnique: vi.fn().mockResolvedValue(baseResource),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn(),
},
},
expected: "Project not found with the given criteria.",
},
{
name: "missing resource",
tx: {
project: {
findUnique: vi.fn().mockResolvedValue(baseProject),
},
resource: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn(),
},
},
expected: "Resource not found with the given criteria.",
},
] as const;
for (const testCase of cases) {
const db = {
project: {
findUnique: vi.fn().mockResolvedValue(baseProject),
},
resource: {
findUnique: vi.fn().mockResolvedValue(baseResource),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn(),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async (callback: (tx: typeof testCase.tx) => unknown) => callback(testCase.tx)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"quick_assign_timeline_resource",
JSON.stringify({
resourceIdentifier: "resource_1",
projectIdentifier: "project_1",
startDate: "2026-03-16",
endDate: "2026-03-20",
hoursPerDay: 8,
}),
ctx,
);
expect(JSON.parse(result.content)).toEqual({ error: testCase.expected });
}
});
it("returns stable validation errors for timeline mutation date ranges", async () => {
const baseProject = {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
};
const baseResource = {
id: "resource_1",
eid: "E-001",
displayName: "Alice",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
};
const baseAssignment = {
id: "assignment_1",
demandRequirementId: null,
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
hoursPerDay: 4,
percentage: 50,
role: "Compositor",
roleId: "role_comp",
dailyCostCents: 20000,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
resource: {
...baseResource,
},
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
demandRequirement: null,
};
const cases = [
{
toolName: "update_timeline_allocation_inline",
payload: {
allocationId: "assignment_1",
startDate: "2026-03-20",
endDate: "2026-03-16",
},
db: {
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(baseAssignment),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
id: "resource_1",
lcrCents: 5000,
availability: baseResource.availability,
}),
},
},
},
{
toolName: "quick_assign_timeline_resource",
payload: {
resourceIdentifier: "resource_1",
projectIdentifier: "project_1",
startDate: "2026-03-20",
endDate: "2026-03-16",
hoursPerDay: 8,
},
db: {
project: {
findUnique: vi.fn().mockResolvedValue(baseProject),
},
resource: {
findUnique: vi.fn().mockResolvedValue(baseResource),
},
},
},
{
toolName: "batch_quick_assign_timeline_resources",
payload: {
assignments: [
{
resourceIdentifier: "resource_1",
projectIdentifier: "project_1",
startDate: "2026-03-20",
endDate: "2026-03-16",
hoursPerDay: 8,
},
],
},
db: {
project: {
findUnique: vi.fn().mockResolvedValue(baseProject),
},
resource: {
findUnique: vi.fn().mockResolvedValue(baseResource),
},
},
},
] as const;
for (const testCase of cases) {
const ctx = createToolContext(
testCase.db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
testCase.toolName,
JSON.stringify(testCase.payload),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: "End date must be after start date",
});
}
});
it("batch quick-assigns timeline resources through the real timeline router mutation", async () => {
const db = {
project: {
findUnique: vi.fn().mockResolvedValue({
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
}),
},
resource: {
findUnique: vi.fn().mockImplementation(async ({ where }: { where: { id: string } }) => ({
id: where.id,
eid: `E-${where.id}`,
displayName: `Resource ${where.id}`,
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
})),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
create: vi
.fn()
.mockResolvedValueOnce({ id: "assignment_batch_1" })
.mockResolvedValueOnce({ id: "assignment_batch_2" }),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"batch_quick_assign_timeline_resources",
JSON.stringify({
assignments: [
{
resourceIdentifier: "resource_1",
projectIdentifier: "project_1",
startDate: "2026-03-16",
endDate: "2026-03-20",
hoursPerDay: 8,
},
{
resourceIdentifier: "resource_2",
projectIdentifier: "project_1",
startDate: "2026-03-23",
endDate: "2026-03-27",
hoursPerDay: 6,
},
],
}),
ctx,
);
expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] });
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
success: true,
count: 2,
}),
);
expect(db.assignment.create).toHaveBeenCalledTimes(2);
});
it("returns a structured batch assignment resolver error for an unknown resource", async () => {
const db = {
project: {
findUnique: vi.fn().mockResolvedValue({
id: "project_1",
shortCode: "GDM",
name: "Gelddruckmaschine",
status: "ACTIVE",
responsiblePerson: null,
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
}),
findFirst: vi.fn().mockResolvedValue(null),
},
resource: {
findUnique: vi.fn().mockResolvedValue(null),
findFirst: vi.fn().mockResolvedValue(null),
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
create: vi.fn(),
},
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"batch_quick_assign_timeline_resources",
JSON.stringify({
assignments: [
{
resourceIdentifier: "missing_resource",
projectIdentifier: "project_1",
startDate: "2026-03-16",
endDate: "2026-03-20",
hoursPerDay: 8,
},
],
}),
ctx,
);
expect(result.action).toBeUndefined();
expect(JSON.parse(result.content)).toEqual({
error: "assignments[0].resourceIdentifier: Resource not found: missing_resource",
field: "assignments[0].resourceIdentifier",
index: 0,
});
expect(db.assignment.create).not.toHaveBeenCalled();
});
it("applies timeline project shifts through the real timeline router mutation", async () => {
const { listAssignmentBookings } = await import("@capakraken/application");
vi.mocked(listAssignmentBookings).mockResolvedValueOnce([]);
const db = {
project: {
findUnique: vi.fn().mockResolvedValue({
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
budgetCents: 100000,
winProbability: 100,
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
}),
update: vi.fn().mockResolvedValue({
id: "project_1",
startDate: new Date("2026-03-23"),
endDate: new Date("2026-03-27"),
}),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"apply_timeline_project_shift",
JSON.stringify({
projectIdentifier: "project_1",
newStartDate: "2026-03-23",
newEndDate: "2026-03-27",
}),
ctx,
);
expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] });
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
success: true,
project: expect.objectContaining({
id: "project_1",
startDate: "2026-03-23",
endDate: "2026-03-27",
}),
validation: expect.objectContaining({
valid: true,
}),
}),
);
expect(db.project.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: "project_1" },
}),
);
});
it("returns a stable project-not-found error if the timeline shift target disappears mid-mutation", async () => {
const { listAssignmentBookings } = await import("@capakraken/application");
vi.mocked(listAssignmentBookings).mockResolvedValueOnce([]);
const db = {
project: {
findUnique: vi
.fn()
.mockResolvedValueOnce({
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: null,
budgetCents: 100000,
winProbability: 100,
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
})
.mockResolvedValueOnce(null),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"apply_timeline_project_shift",
JSON.stringify({
projectIdentifier: "project_1",
newStartDate: "2026-03-23",
newEndDate: "2026-03-27",
}),
ctx,
);
expect(result.action).toBeUndefined();
expect(JSON.parse(result.content)).toEqual({
error: "Project not found with the given criteria.",
});
});
it("batch-shifts timeline allocations through the real timeline router mutation", async () => {
const existingAssignment = {
id: "assignment_1",
demandRequirementId: null,
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
hoursPerDay: 4,
percentage: 50,
role: "Compositor",
roleId: "role_comp",
dailyCostCents: 20000,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
resource: {
id: "resource_1",
displayName: "Alice",
eid: "E-001",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
},
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
demandRequirement: null,
};
const db = {
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(existingAssignment),
update: vi.fn().mockResolvedValue({
...existingAssignment,
startDate: new Date("2026-03-18"),
endDate: new Date("2026-03-22"),
}),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"batch_shift_timeline_allocations",
JSON.stringify({
allocationIds: ["assignment_1"],
daysDelta: 2,
mode: "move",
}),
ctx,
);
expect(result.action).toEqual({ type: "invalidate", scope: ["allocation", "timeline", "project"] });
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
success: true,
count: 1,
}),
);
expect(db.assignment.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: "assignment_1" },
}),
);
});
it("returns a stable allocation-not-found error for inline timeline updates", async () => {
const db = {
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(null),
},
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"update_timeline_allocation_inline",
JSON.stringify({
allocationId: "assignment_missing",
hoursPerDay: 6,
}),
ctx,
);
expect(result.action).toBeUndefined();
expect(JSON.parse(result.content)).toEqual({
error: "Allocation not found with the given criteria.",
});
});
it("returns stable allocation-not-found errors when timeline allocation persistence loses the row", async () => {
const existingAssignment = {
id: "assignment_1",
demandRequirementId: null,
resourceId: "resource_1",
projectId: "project_1",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-20"),
hoursPerDay: 4,
percentage: 50,
role: "Compositor",
roleId: "role_comp",
dailyCostCents: 20000,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
resource: {
id: "resource_1",
displayName: "Alice",
eid: "E-001",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
},
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
demandRequirement: null,
};
const missingDuringUpdate = {
code: "P2025",
message: "Record to update not found",
meta: { modelName: "Assignment" },
};
const updateInlineCtx = createToolContext(
{
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(existingAssignment),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
id: "resource_1",
lcrCents: 5000,
availability: existingAssignment.resource.availability,
}),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
$transaction: vi.fn(async () => {
throw missingDuringUpdate;
}),
},
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const updateInlineResult = await executeTool(
"update_timeline_allocation_inline",
JSON.stringify({
allocationId: "assignment_1",
hoursPerDay: 6,
}),
updateInlineCtx,
);
expect(JSON.parse(updateInlineResult.content)).toEqual({
error: "Allocation not found with the given criteria.",
});
const batchShiftCtx = createToolContext(
{
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(existingAssignment),
},
auditLog: {
create: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn(async () => {
throw missingDuringUpdate;
}),
},
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const batchShiftResult = await executeTool(
"batch_shift_timeline_allocations",
JSON.stringify({
allocationIds: ["assignment_1"],
daysDelta: 2,
mode: "move",
}),
batchShiftCtx,
);
expect(JSON.parse(batchShiftResult.content)).toEqual({
error: "Allocation not found with the given criteria.",
});
});
it("returns a stable allocation-not-found error for batch timeline shifts without matches", async () => {
const db = {
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(null),
},
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"batch_shift_timeline_allocations",
JSON.stringify({
allocationIds: ["assignment_missing"],
daysDelta: 2,
mode: "move",
}),
ctx,
);
expect(result.action).toBeUndefined();
expect(JSON.parse(result.content)).toEqual({
error: "Allocation not found with the given criteria.",
});
});
it("returns a stable demand-requirement-not-found error for batch timeline shifts", async () => {
const demandRequirement = {
id: "demand_requirement_1",
startDate: new Date("2026-03-10"),
endDate: new Date("2026-03-14"),
};
const db = {
allocation: {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockResolvedValue(demandRequirement),
},
assignment: {
findUnique: vi.fn().mockResolvedValue(null),
},
$transaction: vi.fn(async () => {
throw new TRPCError({
code: "NOT_FOUND",
message: "Demand requirement not found",
});
}),
};
const ctx = createToolContext(
db,
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
SystemRole.MANAGER,
);
const result = await executeTool(
"batch_shift_timeline_allocations",
JSON.stringify({
allocationIds: ["demand_requirement_missing"],
daysDelta: 3,
mode: "move",
}),
ctx,
);
expect(result.action).toBeUndefined();
expect(JSON.parse(result.content)).toEqual({
error: "Demand requirement not found with the given criteria.",
});
});
it("returns the chargeability report readmodel through the assistant", async () => {
const { listAssignmentBookings } = await import("@capakraken/application");
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_confirmed",
projectId: "project_confirmed",
resourceId: "resource_1",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-06T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_confirmed",
name: "Confirmed Project",
shortCode: "CP",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
]);
const ctx = createToolContext(
{
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_1",
eid: "E-001",
displayName: "Alice",
fte: 1,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
countryId: "country_es",
federalState: null,
metroCityId: "city_1",
chargeabilityTarget: 80,
country: {
id: "country_es",
code: "ES",
dailyWorkingHours: 8,
scheduleRules: null,
},
orgUnit: { id: "org_1", name: "CGI" },
managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 },
managementLevel: { id: "level_1", name: "L7" },
metroCity: { id: "city_1", name: "Barcelona" },
},
]),
},
project: {
findMany: vi.fn().mockResolvedValue([
{ id: "project_confirmed", utilizationCategory: { code: "Chg" } },
]),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
},
[PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"get_chargeability_report",
JSON.stringify({
startMonth: "2026-03",
endMonth: "2026-03",
resourceLimit: 10,
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
monthKeys: string[];
groupTotals: Array<{ monthKey: string; chargeabilityPct: number; targetPct: number }>;
resourceCount: number;
returnedResourceCount: number;
truncated: boolean;
resources: Array<{
displayName: string;
targetPct: number;
months: Array<{ monthKey: string; sah: number; chargeabilityPct: number }>;
}>;
};
expect(parsed.monthKeys).toEqual(["2026-03"]);
expect(parsed.groupTotals).toEqual([
expect.objectContaining({
monthKey: "2026-03",
chargeabilityPct: expect.any(Number),
targetPct: 80,
}),
]);
expect(parsed.resourceCount).toBe(1);
expect(parsed.returnedResourceCount).toBe(1);
expect(parsed.truncated).toBe(false);
expect(parsed.resources).toEqual([
expect.objectContaining({
displayName: "Alice",
targetPct: 80,
months: [
expect.objectContaining({
monthKey: "2026-03",
sah: expect.any(Number),
chargeabilityPct: expect.any(Number),
}),
],
}),
]);
});
it("returns a filtered resource computation graph through the assistant", async () => {
const resourceRecord = {
id: "resource_augsburg",
displayName: "Bruce Banner",
eid: "bruce.banner",
fte: 1,
lcrCents: 5_000,
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "BY",
metroCityId: "city_augsburg",
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
country: {
id: "country_de",
code: "DE",
name: "Deutschland",
dailyWorkingHours: 8,
scheduleRules: null,
},
metroCity: { id: "city_augsburg", name: "Augsburg" },
managementLevelGroup: {
id: "mlg_1",
name: "Senior",
targetPercentage: 0.8,
},
};
const ctx = createToolContext(
{
resource: {
findUnique: vi.fn().mockResolvedValue(resourceRecord),
findFirst: vi.fn(),
findUniqueOrThrow: vi.fn().mockResolvedValue(resourceRecord),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
calculationRule: {
findMany: vi.fn().mockResolvedValue([]),
},
},
[PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"get_resource_computation_graph",
JSON.stringify({
resourceId: "resource_augsburg",
month: "2026-08",
domain: "SAH",
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
resource: { id: string; displayName: string };
requestedDomain: string;
totalNodeCount: number;
selectedNodeCount: number;
nodes: Array<{ id: string; domain: string }>;
meta: {
countryCode: string | null;
federalState: string | null;
metroCityName: string | null;
resolvedHolidays: Array<{ name: string; scope: string }>;
};
};
expect(parsed.resource).toEqual({
id: "resource_augsburg",
eid: "bruce.banner",
displayName: "Bruce Banner",
});
expect(parsed.requestedDomain).toBe("SAH");
expect(parsed.totalNodeCount).toBeGreaterThan(parsed.selectedNodeCount);
expect(parsed.selectedNodeCount).toBeGreaterThan(0);
expect(parsed.nodes.every((node) => node.domain === "SAH")).toBe(true);
expect(parsed.meta).toMatchObject({
countryCode: "DE",
federalState: "BY",
metroCityName: "Augsburg",
});
expect(parsed.meta.resolvedHolidays).toEqual(expect.arrayContaining([
expect.objectContaining({
name: "Augsburger Friedensfest",
scope: "CITY",
}),
]));
});
it("returns a filtered project computation graph through the assistant", async () => {
const projectRecord = {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
budgetCents: 100_000,
winProbability: 75,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-02-28T00:00:00.000Z"),
status: "ACTIVE",
responsiblePerson: "Larissa Joos",
};
const ctx = createToolContext(
{
project: {
findUnique: vi.fn().mockResolvedValue(projectRecord),
findFirst: vi.fn(),
findUniqueOrThrow: vi.fn().mockResolvedValue(projectRecord),
},
estimate: {
findFirst: vi.fn().mockResolvedValue(null),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
status: "CONFIRMED",
dailyCostCents: 4_000,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-30T00:00:00.000Z"),
hoursPerDay: 4,
},
]),
},
effortRule: {
count: vi.fn().mockResolvedValue(0),
},
experienceMultiplierRule: {
count: vi.fn().mockResolvedValue(0),
},
},
[PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"get_project_computation_graph",
JSON.stringify({
projectId: "project_1",
domain: "BUDGET",
includeLinks: true,
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
project: { id: string; shortCode: string; name: string };
requestedDomain: string;
totalNodeCount: number;
selectedNodeCount: number;
selectedLinkCount: number;
nodes: Array<{ id: string; domain: string }>;
links: Array<{ source: string; target: string }>;
meta: { projectName: string; projectCode: string };
};
expect(parsed.project).toEqual({
id: "project_1",
shortCode: "GDM",
name: "Gelddruckmaschine",
});
expect(parsed.meta).toEqual({
projectName: "Gelddruckmaschine",
projectCode: "GDM",
});
expect(parsed.requestedDomain).toBe("BUDGET");
expect(parsed.totalNodeCount).toBeGreaterThan(parsed.selectedNodeCount);
expect(parsed.selectedNodeCount).toBeGreaterThan(0);
expect(parsed.selectedLinkCount).toBeGreaterThan(0);
expect(parsed.nodes.every((node) => node.domain === "BUDGET")).toBe(true);
expect(parsed.links.length).toBe(parsed.selectedLinkCount);
});
it("scopes assistant notification listing to the current user through the router path", async () => {
const findMany = vi.fn().mockResolvedValue([]);
const ctx = createToolContext({
user: {
findUnique: vi.fn().mockResolvedValue({ id: "user_1" }),
},
notification: {
findMany,
},
});
await executeTool("list_notifications", JSON.stringify({ unreadOnly: true }), ctx);
expect(findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
userId: "user_1",
readAt: null,
}),
}),
);
});
it("scopes mark_notification_read mutations to the current user through the router path", async () => {
const update = vi.fn();
const ctx = createToolContext({
user: {
findUnique: vi.fn().mockResolvedValue({ id: "user_1" }),
},
notification: {
update,
},
});
await executeTool(
"mark_notification_read",
JSON.stringify({ notificationId: "notif_1" }),
ctx,
);
expect(update).toHaveBeenCalledWith({
where: { id: "notif_1", userId: "user_1" },
data: expect.objectContaining({
readAt: expect.any(Date),
}),
});
});
it("requires admin role before listing users through the assistant", async () => {
const findMany = vi.fn();
const ctx = createToolContext({
user: {
findMany,
},
}, [], SystemRole.MANAGER);
const result = await executeTool("list_users", JSON.stringify({ limit: 10 }), ctx);
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
error: "You do not have permission to perform this action.",
}),
);
expect(findMany).not.toHaveBeenCalled();
});
});