import { beforeEach, describe, expect, it, vi } from "vitest"; import { listAssignmentBookings } from "@capakraken/application"; import { PermissionKey, SystemRole } from "@capakraken/shared"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, approveEstimateVersion: vi.fn(), cloneEstimate: vi.fn(), commitDispoImportBatch: vi.fn(), countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), createEstimateExport: vi.fn(), createEstimatePlanningHandoff: vi.fn(), createEstimateRevision: vi.fn(), assessDispoImportReadiness: vi.fn(), loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()), getDashboardDemand: vi.fn().mockResolvedValue([]), getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), getDashboardOverview: vi.fn(), getDashboardSkillGapSummary: vi.fn().mockResolvedValue({ roleGaps: [], totalOpenPositions: 0, skillSupplyTop10: [], resourcesByRole: [], }), getDashboardProjectHealth: vi.fn().mockResolvedValue([]), getDashboardPeakTimes: vi.fn().mockResolvedValue([]), getDashboardTopValueResources: vi.fn().mockResolvedValue([]), getEstimateById: vi.fn(), listAssignmentBookings: vi.fn().mockResolvedValue([]), stageDispoImportBatch: vi.fn(), submitEstimateVersion: vi.fn(), updateEstimateDraft: vi.fn(), }; }); import { executeTool } from "../router/assistant-tools.js"; import { createToolContext } from "./assistant-tools-insights-scenarios-test-helpers.js"; describe("assistant tools workflow scenarios", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(listAssignmentBookings).mockResolvedValue([]); }); it("capacity search then resource check: finds capacity and then checks a specific resource from the result", async () => { vi.mocked(listAssignmentBookings).mockResolvedValue([]); const resourceRecord = { id: "res_1", displayName: "Bruce Banner", eid: "EMP-001", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 }, countryId: "country_de", federalState: null, metroCityId: null, country: { code: "DE", dailyWorkingHours: 8 }, metroCity: null, areaRole: { name: "Pipeline TD" }, chapter: "Delivery", }; const db = { resource: { findMany: vi.fn().mockResolvedValue([resourceRecord]), findUnique: vi.fn().mockResolvedValue(resourceRecord), findFirst: vi.fn().mockResolvedValue(null), }, assignment: { findMany: vi.fn().mockResolvedValue([]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER, permissions: [PermissionKey.VIEW_PLANNING], }); // Step 1: find capacity for the period const capacityResult = await executeTool( "find_capacity", JSON.stringify({ startDate: "2026-05-01", endDate: "2026-05-30", minHoursPerDay: 4, chapter: "Delivery", }), ctx, ); const capacityParsed = JSON.parse(capacityResult.content) as { results: Array<{ id: string; name: string; eid: string }>; totalFound: number; }; expect(db.resource.findMany).toHaveBeenCalled(); expect(capacityParsed.results).toHaveLength(1); expect(capacityParsed.results[0].name).toBe("Bruce Banner"); // Step 2: use resource ID from result to check availability in a specific date window const foundResourceId = capacityParsed.results[0].id; expect(foundResourceId).toBe("res_1"); const availabilityResult = await executeTool( "check_resource_availability", JSON.stringify({ resourceId: foundResourceId, startDate: "2026-05-05", endDate: "2026-05-09", }), ctx, ); const availabilityParsed = JSON.parse(availabilityResult.content) as { workingDays: number; periodAvailableHours: number; periodBookedHours: number; periodRemainingHours: number; isFullyAvailable: boolean; }; // 2026-05-05 (Tue) to 2026-05-09 (Sat) = 4 working days (Tue-Fri) expect(availabilityParsed.workingDays).toBe(4); expect(availabilityParsed.periodAvailableHours).toBe(32); expect(availabilityParsed.periodBookedHours).toBe(0); expect(availabilityParsed.periodRemainingHours).toBe(32); expect(availabilityParsed.isFullyAvailable).toBe(true); }); it("vacation balance then upcoming vacations: retrieves balance and then lists upcoming vacations for the same resource", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-01T00:00:00.000Z")); try { const db = { resource: { findUnique: vi.fn().mockResolvedValue({ id: "res_2", eid: "EMP-002", displayName: "Tony Stark", userId: "user_2", chapter: "Engineering", federalState: null, countryId: "country_de", metroCityId: null, country: { code: "DE", name: "Germany" }, metroCity: null, }), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue({ vacationDefaultDays: 28 }), }, vacationEntitlement: { findUnique: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({ id: "ent_2026_res2", resourceId: "res_2", year: 2026, entitledDays: 28, carryoverDays: 0, usedDays: 0, pendingDays: 0, }), update: vi.fn().mockResolvedValue({ id: "ent_2026_res2", resourceId: "res_2", year: 2026, entitledDays: 28, carryoverDays: 0, usedDays: 0, pendingDays: 0, }), }, vacation: { findMany: vi.fn().mockImplementation(async (args: { where?: { type?: unknown; status?: unknown; startDate?: unknown } } = {}) => { if (args?.where?.status === "APPROVED" && args?.where?.startDate) { // list_vacations_upcoming query return [ { id: "vac_1", resourceId: "res_2", status: "APPROVED", type: "ANNUAL", startDate: new Date("2026-05-10T00:00:00.000Z"), endDate: new Date("2026-05-14T00:00:00.000Z"), isHalfDay: false, halfDayPart: null, resource: { id: "res_2", displayName: "Tony Stark", eid: "EMP-002", lcrCents: 9_000, chapter: "Engineering", }, requestedBy: null, approvedBy: null, }, ]; } // vacation balance queries (by type) return []; }), }, }; const ctx = createToolContext(db, { userRole: SystemRole.ADMIN }); // Step 1: get vacation balance for the resource const balanceResult = await executeTool( "get_vacation_balance", JSON.stringify({ resourceId: "res_2", year: 2026 }), ctx, ); const balanceParsed = JSON.parse(balanceResult.content) as { resource: string; eid: string; year: number; entitlement: number; remaining: number; }; expect(balanceParsed.resource).toBe("Tony Stark"); expect(balanceParsed.eid).toBe("EMP-002"); expect(balanceParsed.year).toBe(2026); expect(balanceParsed.entitlement).toBe(28); expect(balanceParsed.remaining).toBe(28); // Step 2: list upcoming vacations for the same resource (filter by name) const upcomingResult = await executeTool( "list_vacations_upcoming", JSON.stringify({ resourceName: "Tony", daysAhead: 30, limit: 10 }), ctx, ); const upcomingParsed = JSON.parse(upcomingResult.content) as Array<{ resource: string; eid: string; start: string; end: string; }>; expect(upcomingParsed).toHaveLength(1); expect(upcomingParsed[0].resource).toBe("Tony Stark"); expect(upcomingParsed[0].eid).toBe("EMP-002"); expect(upcomingParsed[0].start).toBe("2026-05-10"); expect(upcomingParsed[0].end).toBe("2026-05-14"); } finally { vi.useRealTimers(); } }); it("demand listing then staffing suggestions: lists open demands and then retrieves suggestions for the project", async () => { vi.mocked(listAssignmentBookings).mockResolvedValue([]); const projectRecord = { id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", status: "ACTIVE", responsiblePerson: null, startDate: new Date("2026-06-01T00:00:00.000Z"), endDate: new Date("2026-06-30T00:00:00.000Z"), }; const db = { demandRequirement: { findMany: vi.fn().mockResolvedValue([ { id: "demand_1", projectId: "project_1", startDate: new Date("2026-06-01T00:00:00.000Z"), endDate: new Date("2026-06-30T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: "Pipeline TD", roleId: null, headcount: 2, status: "OPEN", metadata: null, createdAt: new Date("2026-05-01T00:00:00.000Z"), updatedAt: new Date("2026-05-01T00:00:00.000Z"), project: { id: "project_1", name: "Gelddruckmaschine", shortCode: "GDM", status: "ACTIVE", endDate: new Date("2026-06-30T00:00:00.000Z"), }, roleEntity: null, assignments: [], }, ]), }, project: { findUnique: vi.fn().mockResolvedValue(projectRecord), findFirst: vi.fn().mockResolvedValue(null), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_3", displayName: "Natasha Romanoff", eid: "EMP-003", fte: 1, chapter: "VFX", skills: [{ skill: "Houdini", category: "FX", proficiency: 4, isMainSkill: true }], lcrCents: 7_500, chargeabilityTarget: 80, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 }, valueScore: 85, countryId: null, federalState: null, metroCityId: null, country: null, metroCity: null, areaRole: { name: "Pipeline TD" }, }, ]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER, permissions: [PermissionKey.VIEW_PLANNING, PermissionKey.VIEW_COSTS], }); // Step 1: list demands (no status filter — AllocationStatus enum doesn't include OPEN) const demandsResult = await executeTool( "list_demands", JSON.stringify({}), ctx, ); const demandsParsed = JSON.parse(demandsResult.content) as Array<{ id: string; project: string; role: string; status: string; headcount: number; remaining: number; }>; expect(demandsParsed).toHaveLength(1); expect(demandsParsed[0].id).toBe("demand_1"); expect(demandsParsed[0].project).toBe("Gelddruckmaschine"); expect(demandsParsed[0].status).toBe("OPEN"); expect(demandsParsed[0].headcount).toBe(2); expect(demandsParsed[0].remaining).toBe(2); // Step 2: get staffing suggestions for the project from the demand const projectId = demandsParsed[0].id.startsWith("demand_") ? "project_1" : demandsParsed[0].id; const suggestionsResult = await executeTool( "get_staffing_suggestions", JSON.stringify({ projectId, startDate: "2026-06-01", endDate: "2026-06-30", limit: 5, }), ctx, ); const suggestionsParsed = JSON.parse(suggestionsResult.content) as { project: string; suggestions: Array<{ id: string; name: string }>; }; expect(suggestionsParsed.project).toBe("Gelddruckmaschine (GDM)"); expect(Array.isArray(suggestionsParsed.suggestions)).toBe(true); }); it("budget status check: retrieves budget utilization for a project", async () => { vi.mocked(listAssignmentBookings).mockResolvedValue([ { projectId: "project_2", status: "CONFIRMED", dailyCostCents: 12_000, hoursPerDay: 8, startDate: new Date("2026-07-01T00:00:00.000Z"), endDate: new Date("2026-07-02T00:00:00.000Z"), project: { id: "project_2", status: "ACTIVE", }, }, { projectId: "project_2", status: "CONFIRMED", dailyCostCents: 8_000, hoursPerDay: 8, startDate: new Date("2026-07-03T00:00:00.000Z"), endDate: new Date("2026-07-03T00:00:00.000Z"), project: { id: "project_2", status: "ACTIVE", }, }, ] as Awaited>); const db = { project: { findUnique: vi .fn() .mockResolvedValueOnce({ id: "project_2", name: "Raketenbauprojekt", shortCode: "RBP", status: "ACTIVE", responsiblePerson: "Elon", }) .mockResolvedValueOnce({ id: "project_2", name: "Raketenbauprojekt", shortCode: "RBP", budgetCents: 500_000, winProbability: 100, startDate: new Date("2026-07-01T00:00:00.000Z"), endDate: new Date("2026-07-31T00:00:00.000Z"), }), findFirst: vi.fn(), }, assignment: { findMany: vi.fn().mockResolvedValue([]), }, }; const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER, permissions: [PermissionKey.VIEW_COSTS], }); const result = await executeTool( "get_budget_status", JSON.stringify({ projectId: "project_2" }), ctx, ); const parsed = JSON.parse(result.content) as { project: string; code: string; budget: string; confirmed: string; proposed: string; allocated: string; remaining: string; utilization: string; }; expect(vi.mocked(listAssignmentBookings)).toHaveBeenCalledWith(db, { projectIds: ["project_2"], }); expect(parsed.project).toBe("Raketenbauprojekt"); expect(parsed.code).toBe("RBP"); expect(parsed.budget).toBe("5.000,00 EUR"); // Booking 1: July 1-2 (2 days) × 12000 cents = 24000 cents = 240 EUR // Booking 2: July 3 (1 day) × 8000 cents = 8000 cents = 80 EUR // Total confirmed: 320 EUR expect(parsed.confirmed).toBe("320,00 EUR"); expect(parsed.proposed).toBe("0,00 EUR"); expect(parsed.allocated).toBe("320,00 EUR"); expect(parsed.remaining).toBe("4.680,00 EUR"); expect(parsed.utilization).toBe("6.4%"); }); it("search resources with availability filter: finds resources in a chapter", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_4", eid: "EMP-004", displayName: "Wanda Maximoff", chapter: "FX", fte: 1, lcrCents: 8_000, chargeabilityTarget: 80, isActive: true, areaRole: { name: "FX Artist" }, country: { code: "DE", name: "Germany" }, metroCity: null, orgUnit: null, }, { id: "res_5", eid: "EMP-005", displayName: "Pietro Maximoff", chapter: "FX", fte: 0.8, lcrCents: 7_200, chargeabilityTarget: 75, isActive: true, areaRole: { name: "Motion Designer" }, country: { code: "DE", name: "Germany" }, metroCity: null, orgUnit: null, }, ]), }, }; const ctx = createToolContext(db, { userRole: SystemRole.CONTROLLER, permissions: [PermissionKey.VIEW_ALL_RESOURCES], }); const result = await executeTool( "search_resources", JSON.stringify({ orgUnit: undefined, country: "DE", limit: 10 }), ctx, ); const parsed = JSON.parse(result.content) as Array<{ id: string; name: string; eid: string; chapter: string; role: string; }>; expect(db.resource.findMany).toHaveBeenCalled(); expect(parsed).toHaveLength(2); const wanda = parsed.find((r) => r.id === "res_4"); expect(wanda).toBeDefined(); expect(wanda?.name).toBe("Wanda Maximoff"); expect(wanda?.eid).toBe("EMP-004"); expect(wanda?.chapter).toBe("FX"); expect(wanda?.role).toBe("FX Artist"); const pietro = parsed.find((r) => r.id === "res_5"); expect(pietro).toBeDefined(); expect(pietro?.name).toBe("Pietro Maximoff"); expect(pietro?.eid).toBe("EMP-005"); }); });