test(api): harden assistant tool error handling

This commit is contained in:
2026-03-30 11:51:59 +02:00
parent 4ce8577824
commit 7aa32f8a5c
8 changed files with 7898 additions and 381 deletions
@@ -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();