test(api): cover remaining timeline and broadcast fallback races
This commit is contained in:
@@ -963,6 +963,241 @@ describe("assistant advanced tools and scoping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
@@ -1340,6 +1575,128 @@ describe("assistant advanced tools and scoping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
|
||||
@@ -2169,6 +2169,98 @@ describe("assistant import/export and dispo tools", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns stable assistant errors when broadcast fan-out loses sender or recipient rows inside the router transaction", async () => {
|
||||
const senderMissingTx = {
|
||||
notificationBroadcast: {
|
||||
create: vi.fn().mockRejectedValue(
|
||||
Object.assign(new Error("Foreign key constraint failed"), {
|
||||
code: "P2003",
|
||||
meta: { field_name: "NotificationBroadcast_senderId_fkey" },
|
||||
}),
|
||||
),
|
||||
update: vi.fn(),
|
||||
},
|
||||
notification: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
};
|
||||
const senderMissingCtx = createToolContext(
|
||||
{
|
||||
user: {
|
||||
findMany: vi.fn().mockResolvedValue([{ id: "user_2" }]),
|
||||
},
|
||||
$transaction: vi.fn(async (callback: (db: typeof senderMissingTx) => Promise<unknown>) => callback(senderMissingTx)),
|
||||
notificationBroadcast: {
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
notification: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
{ userRole: SystemRole.MANAGER },
|
||||
);
|
||||
|
||||
const senderMissingResult = await executeTool(
|
||||
"send_broadcast",
|
||||
JSON.stringify({
|
||||
title: "Office update",
|
||||
targetType: "all",
|
||||
}),
|
||||
senderMissingCtx,
|
||||
);
|
||||
expect(JSON.parse(senderMissingResult.content)).toEqual({
|
||||
error: "Sender user not found with the given criteria.",
|
||||
});
|
||||
|
||||
const recipientMissingTx = {
|
||||
notificationBroadcast: {
|
||||
create: vi.fn().mockResolvedValue({
|
||||
id: "broadcast_missing_recipient",
|
||||
title: "Office update",
|
||||
targetType: "all",
|
||||
}),
|
||||
update: vi.fn(),
|
||||
},
|
||||
notification: {
|
||||
create: vi.fn().mockRejectedValue(
|
||||
Object.assign(new Error("Foreign key constraint failed"), {
|
||||
code: "P2003",
|
||||
meta: { field_name: "Notification_userId_fkey" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
};
|
||||
const recipientMissingCtx = createToolContext(
|
||||
{
|
||||
user: {
|
||||
findMany: vi.fn().mockResolvedValue([{ id: "user_2" }]),
|
||||
},
|
||||
$transaction: vi.fn(async (callback: (db: typeof recipientMissingTx) => Promise<unknown>) => callback(recipientMissingTx)),
|
||||
notificationBroadcast: {
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
notification: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
{ userRole: SystemRole.MANAGER },
|
||||
);
|
||||
|
||||
const recipientMissingResult = await executeTool(
|
||||
"send_broadcast",
|
||||
JSON.stringify({
|
||||
title: "Office update",
|
||||
targetType: "all",
|
||||
}),
|
||||
recipientMissingCtx,
|
||||
);
|
||||
expect(JSON.parse(recipientMissingResult.content)).toEqual({
|
||||
error: "Broadcast recipient user not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable assistant error when a broadcast target resolves to no recipients", async () => {
|
||||
const create = vi.fn().mockResolvedValue({
|
||||
id: "broadcast_empty",
|
||||
|
||||
Reference in New Issue
Block a user