fix(api): harden broadcast and assistant fallback errors

This commit is contained in:
2026-03-30 12:03:27 +02:00
parent 22cff9648e
commit 6a6e98b5f7
5 changed files with 305 additions and 57 deletions
@@ -1374,6 +1374,51 @@ describe("assistant advanced tools and scoping", () => {
});
});
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([
@@ -2170,14 +2170,15 @@ describe("assistant import/export and dispo tools", () => {
});
it("returns a stable assistant error when a broadcast target resolves to no recipients", async () => {
const create = vi.fn().mockResolvedValue({
id: "broadcast_empty",
title: "Office update",
targetType: "user",
});
const ctx = createToolContext(
{
notificationBroadcast: {
create: vi.fn().mockResolvedValue({
id: "broadcast_empty",
title: "Office update",
targetType: "user",
}),
create,
},
},
{ userRole: SystemRole.MANAGER },
@@ -2195,6 +2196,39 @@ describe("assistant import/export and dispo tools", () => {
expect(JSON.parse(result.content)).toEqual({
error: "No recipients matched the broadcast target.",
});
expect(create).not.toHaveBeenCalled();
});
it("returns a stable assistant error when broadcast creation fails because the sender user is missing", async () => {
const ctx = createToolContext(
{
user: {
findMany: vi.fn().mockResolvedValue([{ id: "user_2" }]),
},
notificationBroadcast: {
create: vi.fn().mockRejectedValue(
Object.assign(new Error("Foreign key constraint failed"), {
code: "P2003",
meta: { field_name: "NotificationBroadcast_senderId_fkey" },
}),
),
},
},
{ userRole: SystemRole.MANAGER },
);
const result = await executeTool(
"send_broadcast",
JSON.stringify({
title: "Office update",
targetType: "all",
}),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: "Sender user not found with the given criteria.",
});
});
it("reads broadcast details through the real notification router and rejects plain users", async () => {
@@ -2770,6 +2804,58 @@ describe("assistant import/export and dispo tools", () => {
setup: () => vi.mocked(updateEstimateDraft).mockRejectedValueOnce(new Error("Estimate has no working version")),
expected: "Estimate has no working version.",
},
{
name: "update_estimate_draft missing scope item reference",
toolName: "update_estimate_draft",
payload: {
id: "est_scope_missing",
baseCurrency: "EUR",
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
},
permission: PermissionKey.MANAGE_PROJECTS,
db: {
estimate: {
findUnique: vi.fn().mockResolvedValue({ projectId: null }),
},
},
setup: () => vi.mocked(updateEstimateDraft).mockRejectedValueOnce(
Object.assign(new Error("Foreign key constraint failed"), {
code: "P2003",
meta: { field_name: "EstimateScopeItem_scopeItemId_fkey" },
}),
),
expected: "Estimate scope item not found with the given criteria.",
},
{
name: "update_estimate_draft generic missing estimate reference",
toolName: "update_estimate_draft",
payload: {
id: "est_reference_missing",
baseCurrency: "EUR",
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
},
permission: PermissionKey.MANAGE_PROJECTS,
db: {
estimate: {
findUnique: vi.fn().mockResolvedValue({ projectId: null }),
},
},
setup: () => vi.mocked(updateEstimateDraft).mockRejectedValueOnce(
Object.assign(new Error("Foreign key constraint failed"), {
code: "P2003",
meta: { field_name: "EstimateVersion_estimateId_fkey" },
}),
),
expected: "One of the referenced project, role, resource, or scope items no longer exists.",
},
{
name: "submit_estimate_version missing version",
toolName: "submit_estimate_version",
@@ -306,13 +306,7 @@ describe("notification.createBroadcast", () => {
message: "No recipients matched the broadcast target.",
});
expect(create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
senderId: "user_mgr",
title: "Ops update",
targetType: "all",
}),
}));
expect(create).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
expect(resolveRecipientsMock).toHaveBeenCalledWith(
"all",