2146 lines
62 KiB
TypeScript
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();
|
|
});
|
|
});
|