import { describe, expect, it, vi } from "vitest"; import { AllocationStatus, PermissionKey, 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([]), }; }); 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, 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 }) => { 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("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("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("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 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: expect.stringContaining("Admin role required"), }), ); expect(findMany).not.toHaveBeenCalled(); }); });