test(api): harden assistant tool error handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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")>();
|
||||
@@ -893,6 +894,75 @@ describe("assistant advanced tools and scoping", () => {
|
||||
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("batch quick-assigns timeline resources through the real timeline router mutation", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
@@ -975,6 +1045,60 @@ describe("assistant advanced tools and scoping", () => {
|
||||
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([]);
|
||||
@@ -1046,6 +1170,56 @@ describe("assistant advanced tools and scoping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -1133,6 +1307,73 @@ describe("assistant advanced tools and scoping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 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 the chargeability report readmodel through the assistant", async () => {
|
||||
const { listAssignmentBookings } = await import("@capakraken/application");
|
||||
vi.mocked(listAssignmentBookings).mockResolvedValue([
|
||||
@@ -1494,7 +1735,7 @@ describe("assistant advanced tools and scoping", () => {
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual(
|
||||
expect.objectContaining({
|
||||
error: expect.stringContaining("Admin role required"),
|
||||
error: "You do not have permission to perform this action.",
|
||||
}),
|
||||
);
|
||||
expect(findMany).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user