chore(repo): checkpoint current capakraken implementation state
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PermissionKey, type PermissionKey as PermissionKeyValue } from "@capakraken/shared";
|
||||
import { PermissionKey, SystemRole, type PermissionKey as PermissionKeyValue } from "@capakraken/shared";
|
||||
import {
|
||||
ASSISTANT_CONFIRMATION_PREFIX,
|
||||
canExecuteMutationTool,
|
||||
@@ -12,8 +12,11 @@ import {
|
||||
} from "../router/assistant.js";
|
||||
import { TOOL_DEFINITIONS } from "../router/assistant-tools.js";
|
||||
|
||||
function getToolNames(permissions: PermissionKeyValue[]) {
|
||||
return getAvailableAssistantTools(new Set(permissions)).map((tool) => tool.function.name);
|
||||
function getToolNames(
|
||||
permissions: PermissionKeyValue[],
|
||||
userRole: SystemRole = SystemRole.ADMIN,
|
||||
) {
|
||||
return getAvailableAssistantTools(new Set(permissions), userRole).map((tool) => tool.function.name);
|
||||
}
|
||||
|
||||
const TEST_USER_ID = "assistant-test-user";
|
||||
@@ -187,6 +190,9 @@ describe("assistant router tool gating", () => {
|
||||
|
||||
expect(withoutAdvanced).not.toContain("find_best_project_resource");
|
||||
expect(withAdvanced).toContain("find_best_project_resource");
|
||||
expect(withAdvanced).toContain("get_chargeability_report");
|
||||
expect(withAdvanced).toContain("get_resource_computation_graph");
|
||||
expect(withAdvanced).toContain("get_project_computation_graph");
|
||||
});
|
||||
|
||||
it("keeps user administration tools behind manageUsers", () => {
|
||||
@@ -201,6 +207,93 @@ describe("assistant router tool gating", () => {
|
||||
const names = getToolNames([PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS]);
|
||||
|
||||
expect(names).not.toContain("find_best_project_resource");
|
||||
expect(names).not.toContain("get_chargeability_report");
|
||||
expect(names).not.toContain("get_resource_computation_graph");
|
||||
expect(names).not.toContain("get_project_computation_graph");
|
||||
});
|
||||
|
||||
it("keeps controller-grade readmodels hidden from plain users while allowing controller roles", () => {
|
||||
const controllerNames = getToolNames([
|
||||
PermissionKey.VIEW_COSTS,
|
||||
PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
||||
], SystemRole.CONTROLLER);
|
||||
const userNames = getToolNames([
|
||||
PermissionKey.VIEW_COSTS,
|
||||
PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
||||
], SystemRole.USER);
|
||||
|
||||
expect(controllerNames).toContain("get_chargeability_report");
|
||||
expect(controllerNames).toContain("get_resource_computation_graph");
|
||||
expect(controllerNames).toContain("get_project_computation_graph");
|
||||
expect(userNames).not.toContain("get_chargeability_report");
|
||||
expect(userNames).not.toContain("get_resource_computation_graph");
|
||||
expect(userNames).not.toContain("get_project_computation_graph");
|
||||
});
|
||||
|
||||
it("keeps timeline write parity tools behind manager/admin role, manageAllocations, and advanced assistant access", () => {
|
||||
const managerNames = getToolNames([
|
||||
PermissionKey.MANAGE_ALLOCATIONS,
|
||||
PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
||||
], SystemRole.MANAGER);
|
||||
const userNames = getToolNames([
|
||||
PermissionKey.MANAGE_ALLOCATIONS,
|
||||
PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
||||
], SystemRole.USER);
|
||||
const missingAdvancedNames = getToolNames([
|
||||
PermissionKey.MANAGE_ALLOCATIONS,
|
||||
], SystemRole.MANAGER);
|
||||
|
||||
expect(managerNames).toContain("update_timeline_allocation_inline");
|
||||
expect(managerNames).toContain("apply_timeline_project_shift");
|
||||
expect(managerNames).toContain("quick_assign_timeline_resource");
|
||||
expect(managerNames).toContain("batch_quick_assign_timeline_resources");
|
||||
expect(managerNames).toContain("batch_shift_timeline_allocations");
|
||||
expect(userNames).not.toContain("update_timeline_allocation_inline");
|
||||
expect(userNames).not.toContain("apply_timeline_project_shift");
|
||||
expect(userNames).not.toContain("quick_assign_timeline_resource");
|
||||
expect(userNames).not.toContain("batch_quick_assign_timeline_resources");
|
||||
expect(userNames).not.toContain("batch_shift_timeline_allocations");
|
||||
expect(missingAdvancedNames).not.toContain("update_timeline_allocation_inline");
|
||||
expect(missingAdvancedNames).not.toContain("quick_assign_timeline_resource");
|
||||
});
|
||||
|
||||
it("keeps holiday calendar mutation tools admin-only while leaving read tools available", () => {
|
||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||
|
||||
expect(adminNames).toContain("list_holiday_calendars");
|
||||
expect(adminNames).toContain("get_holiday_calendar");
|
||||
expect(adminNames).toContain("preview_resolved_holiday_calendar");
|
||||
expect(adminNames).toContain("create_holiday_calendar");
|
||||
expect(managerNames).toContain("list_holiday_calendars");
|
||||
expect(managerNames).toContain("get_holiday_calendar");
|
||||
expect(managerNames).toContain("preview_resolved_holiday_calendar");
|
||||
expect(managerNames).not.toContain("create_holiday_calendar");
|
||||
expect(managerNames).not.toContain("update_holiday_calendar");
|
||||
expect(managerNames).not.toContain("delete_holiday_calendar");
|
||||
expect(managerNames).not.toContain("create_holiday_calendar_entry");
|
||||
expect(managerNames).not.toContain("update_holiday_calendar_entry");
|
||||
expect(managerNames).not.toContain("delete_holiday_calendar_entry");
|
||||
});
|
||||
|
||||
it("keeps country and metro-city mutation tools admin-only while leaving read tools available", () => {
|
||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||
|
||||
expect(adminNames).toContain("list_countries");
|
||||
expect(adminNames).toContain("get_country");
|
||||
expect(adminNames).toContain("create_country");
|
||||
expect(adminNames).toContain("update_country");
|
||||
expect(adminNames).toContain("create_metro_city");
|
||||
expect(adminNames).toContain("update_metro_city");
|
||||
expect(adminNames).toContain("delete_metro_city");
|
||||
expect(managerNames).toContain("list_countries");
|
||||
expect(managerNames).toContain("get_country");
|
||||
expect(managerNames).not.toContain("create_country");
|
||||
expect(managerNames).not.toContain("update_country");
|
||||
expect(managerNames).not.toContain("create_metro_city");
|
||||
expect(managerNames).not.toContain("update_metro_city");
|
||||
expect(managerNames).not.toContain("delete_metro_city");
|
||||
});
|
||||
|
||||
it("blocks mutation tools until the user confirms a prior assistant summary", () => {
|
||||
@@ -397,5 +490,16 @@ describe("assistant router tool gating", () => {
|
||||
expect(toolDescriptions.get("list_users")).toContain("manageUsers");
|
||||
expect(toolDescriptions.get("create_task_for_user")).toContain("manageProjects");
|
||||
expect(toolDescriptions.get("send_broadcast")).toContain("manageProjects");
|
||||
expect(toolDescriptions.get("create_holiday_calendar")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("create_holiday_calendar_entry")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("get_chargeability_report")).toContain("controller/manager/admin");
|
||||
expect(toolDescriptions.get("get_chargeability_report")).toContain("viewCosts");
|
||||
expect(toolDescriptions.get("get_resource_computation_graph")).toContain("useAssistantAdvancedTools");
|
||||
expect(toolDescriptions.get("get_project_computation_graph")).toContain("controller/manager/admin");
|
||||
expect(toolDescriptions.get("update_timeline_allocation_inline")).toContain("manager/admin");
|
||||
expect(toolDescriptions.get("apply_timeline_project_shift")).toContain("manageAllocations");
|
||||
expect(toolDescriptions.get("quick_assign_timeline_resource")).toContain("useAssistantAdvancedTools");
|
||||
expect(toolDescriptions.get("batch_quick_assign_timeline_resources")).toContain("manageAllocations");
|
||||
expect(toolDescriptions.get("batch_shift_timeline_allocations")).toContain("manager/admin");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { PermissionKey } from "@capakraken/shared";
|
||||
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
|
||||
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||
@@ -11,17 +11,43 @@ vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
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: "ADMIN",
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -542,6 +568,686 @@ describe("assistant advanced tools and scoping", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
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("scopes assistant notification listing to the current user", async () => {
|
||||
const findMany = vi.fn().mockResolvedValue([]);
|
||||
const ctx = createToolContext({
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { executeTool, type ToolContext } from "../router/assistant-tools.js";
|
||||
|
||||
function createToolContext(
|
||||
db: Record<string, unknown>,
|
||||
permissions: string[] = [],
|
||||
userRole: SystemRole = SystemRole.ADMIN,
|
||||
): ToolContext {
|
||||
return {
|
||||
db: db as ToolContext["db"],
|
||||
userId: "user_1",
|
||||
userRole,
|
||||
permissions: new Set(permissions) as ToolContext["permissions"],
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant country tools", () => {
|
||||
it("lists countries with schedule rules, active state, and metro cities", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "country_de",
|
||||
code: "DE",
|
||||
name: "Deutschland",
|
||||
dailyWorkingHours: 8,
|
||||
scheduleRules: null,
|
||||
isActive: true,
|
||||
metroCities: [{ id: "city_muc", name: "Munich" }],
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"list_countries",
|
||||
JSON.stringify({ includeInactive: true }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(result.content) as {
|
||||
count: number;
|
||||
countries: Array<{
|
||||
code: string;
|
||||
isActive: boolean;
|
||||
metroCities: Array<{ id: string; name: string }>;
|
||||
cities: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
expect(parsed.count).toBe(1);
|
||||
expect(parsed.countries[0]).toMatchObject({
|
||||
code: "DE",
|
||||
isActive: true,
|
||||
cities: ["Munich"],
|
||||
metroCities: [{ id: "city_muc", name: "Munich" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("gets a country by code and exposes schedule details and resource count", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
findFirst: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
id: "country_es",
|
||||
code: "ES",
|
||||
name: "Spain",
|
||||
dailyWorkingHours: 8,
|
||||
scheduleRules: {
|
||||
type: "spain",
|
||||
fridayHours: 6.5,
|
||||
summerPeriod: { from: "07-01", to: "09-15" },
|
||||
summerHours: 6.5,
|
||||
regularHours: 9,
|
||||
},
|
||||
isActive: true,
|
||||
metroCities: [{ id: "city_mad", name: "Madrid" }],
|
||||
_count: { resources: 4 },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"get_country",
|
||||
JSON.stringify({ identifier: "ES" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(result.content) as {
|
||||
code: string;
|
||||
resourceCount: number | null;
|
||||
scheduleRules: { type: string };
|
||||
metroCities: Array<{ name: string }>;
|
||||
};
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
code: "ES",
|
||||
resourceCount: 4,
|
||||
scheduleRules: { type: "spain" },
|
||||
metroCities: [{ name: "Madrid" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a country for admin users and returns an invalidation action", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
create: vi.fn().mockResolvedValue({
|
||||
id: "country_es",
|
||||
code: "ES",
|
||||
name: "Spain",
|
||||
dailyWorkingHours: 8,
|
||||
scheduleRules: null,
|
||||
isActive: true,
|
||||
metroCities: [],
|
||||
_count: { resources: 0 },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"create_country",
|
||||
JSON.stringify({ code: "ES", name: "Spain", dailyWorkingHours: 8 }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toEqual({
|
||||
type: "invalidate",
|
||||
scope: ["country", "resource", "holidayCalendar", "vacation"],
|
||||
});
|
||||
expect(result.data).toMatchObject({
|
||||
success: true,
|
||||
country: { code: "ES", name: "Spain" },
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses country mutations for non-admin users", async () => {
|
||||
const ctx = createToolContext({ country: {} }, [], SystemRole.MANAGER);
|
||||
|
||||
const result = await executeTool(
|
||||
"create_country",
|
||||
JSON.stringify({ code: "ES", name: "Spain" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Admin role required to perform this action.",
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes metro cities only when no resources are assigned", async () => {
|
||||
const ctx = createToolContext({
|
||||
metroCity: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "city_ham",
|
||||
name: "Hamburg",
|
||||
_count: { resources: 0 },
|
||||
}),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"delete_metro_city",
|
||||
JSON.stringify({ id: "city_ham" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toEqual({
|
||||
type: "invalidate",
|
||||
scope: ["country", "resource", "holidayCalendar", "vacation"],
|
||||
});
|
||||
expect(result.data).toMatchObject({
|
||||
success: true,
|
||||
message: "Deleted metro city: Hamburg",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
|
||||
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||
@@ -8,16 +9,21 @@ vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../lib/audit.js", () => ({
|
||||
createAuditEntry: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
import { executeTool, type ToolContext } from "../router/assistant-tools.js";
|
||||
|
||||
function createToolContext(
|
||||
db: Record<string, unknown>,
|
||||
permissions: string[] = [],
|
||||
userRole: SystemRole = SystemRole.ADMIN,
|
||||
): ToolContext {
|
||||
return {
|
||||
db: db as ToolContext["db"],
|
||||
userId: "user_1",
|
||||
userRole: "ADMIN",
|
||||
userRole,
|
||||
permissions: new Set(permissions) as ToolContext["permissions"],
|
||||
};
|
||||
}
|
||||
@@ -107,6 +113,193 @@ describe("assistant holiday tools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("lists holiday calendars with scope metadata and entry counts", async () => {
|
||||
const ctx = createToolContext({
|
||||
holidayCalendar: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "cal_by",
|
||||
name: "Bayern Feiertage",
|
||||
scopeType: "STATE",
|
||||
stateCode: "BY",
|
||||
isActive: true,
|
||||
priority: 10,
|
||||
country: { id: "country_de", code: "DE", name: "Deutschland" },
|
||||
metroCity: null,
|
||||
_count: { entries: 2 },
|
||||
entries: [
|
||||
{
|
||||
id: "entry_1",
|
||||
date: new Date("2026-01-06T00:00:00.000Z"),
|
||||
name: "Heilige Drei Koenige",
|
||||
isRecurringAnnual: true,
|
||||
source: "state",
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"list_holiday_calendars",
|
||||
JSON.stringify({ countryCode: "DE", scopeType: "STATE", includeInactive: true }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(result.content) as {
|
||||
count: number;
|
||||
calendars: Array<{
|
||||
name: string;
|
||||
scopeType: string;
|
||||
stateCode: string | null;
|
||||
entryCount: number;
|
||||
country: { code: string };
|
||||
}>;
|
||||
};
|
||||
|
||||
expect(parsed.count).toBe(1);
|
||||
expect(parsed.calendars).toHaveLength(1);
|
||||
expect(parsed.calendars[0]).toMatchObject({
|
||||
name: "Bayern Feiertage",
|
||||
scopeType: "STATE",
|
||||
stateCode: "BY",
|
||||
entryCount: 2,
|
||||
country: { code: "DE" },
|
||||
});
|
||||
});
|
||||
|
||||
it("previews resolved holiday calendars for a scope and shows the source calendar", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue({ code: "DE" }),
|
||||
},
|
||||
metroCity: {
|
||||
findUnique: vi.fn().mockResolvedValue({ name: "Augsburg" }),
|
||||
},
|
||||
holidayCalendar: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "cal_city",
|
||||
name: "Augsburg lokal",
|
||||
scopeType: "CITY",
|
||||
priority: 5,
|
||||
createdAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
entries: [
|
||||
{
|
||||
id: "entry_1",
|
||||
date: new Date("2020-08-08T00:00:00.000Z"),
|
||||
name: "Friedensfest lokal",
|
||||
isRecurringAnnual: true,
|
||||
source: "manual",
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"preview_resolved_holiday_calendar",
|
||||
JSON.stringify({ countryId: "country_de", metroCityId: "city_augsburg", year: 2026 }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(result.content) as {
|
||||
count: number;
|
||||
locationContext: { countryCode: string; metroCity: string | null; year: number };
|
||||
holidays: Array<{ name: string; calendarName: string; scope: string; date: string }>;
|
||||
};
|
||||
|
||||
expect(parsed.count).toBeGreaterThan(0);
|
||||
expect(parsed.locationContext).toEqual(
|
||||
expect.objectContaining({
|
||||
countryCode: "DE",
|
||||
metroCity: "Augsburg",
|
||||
year: 2026,
|
||||
}),
|
||||
);
|
||||
expect(parsed.holidays).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "Friedensfest lokal",
|
||||
calendarName: "Augsburg lokal",
|
||||
scope: "CITY",
|
||||
date: "2026-08-08",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a holiday calendar through the assistant for admin users", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Deutschland" }),
|
||||
},
|
||||
holidayCalendar: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
create: vi.fn().mockResolvedValue({
|
||||
id: "cal_by",
|
||||
name: "Bayern Feiertage",
|
||||
scopeType: "STATE",
|
||||
stateCode: "BY",
|
||||
isActive: true,
|
||||
priority: 10,
|
||||
country: { id: "country_de", code: "DE", name: "Deutschland" },
|
||||
metroCity: null,
|
||||
entries: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"create_holiday_calendar",
|
||||
JSON.stringify({
|
||||
name: "Bayern Feiertage",
|
||||
scopeType: "STATE",
|
||||
countryId: "country_de",
|
||||
stateCode: "BY",
|
||||
priority: 10,
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(result.content) as {
|
||||
success: boolean;
|
||||
message: string;
|
||||
calendar: { name: string; stateCode: string | null };
|
||||
};
|
||||
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.message).toContain("Created holiday calendar");
|
||||
expect(parsed.calendar).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "Bayern Feiertage",
|
||||
stateCode: "BY",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects holiday calendar mutations for non-admin assistant users", async () => {
|
||||
const ctx = createToolContext({}, [], SystemRole.MANAGER);
|
||||
const result = await executeTool(
|
||||
"create_holiday_calendar",
|
||||
JSON.stringify({
|
||||
name: "Hamburg Feiertage",
|
||||
scopeType: "STATE",
|
||||
countryId: "country_de",
|
||||
stateCode: "HH",
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual(
|
||||
expect.objectContaining({
|
||||
error: "Admin role required to perform this action.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("calculates chargeability with regional holidays excluded from booked and available hours", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
|
||||
@@ -19,9 +19,13 @@ vi.mock("@capakraken/staffing", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@capakraken/application", () => ({
|
||||
listAssignmentBookings: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||
return {
|
||||
...actual,
|
||||
listAssignmentBookings: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
const createCaller = createCallerFactory(staffingRouter);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user