test(api): cover assistant timeline allocation shifts

This commit is contained in:
2026-04-01 00:35:16 +02:00
parent adf25f328f
commit 3607d73b84
3 changed files with 275 additions and 0 deletions
@@ -0,0 +1,138 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionKey, SystemRole } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import {
createExistingAssignment,
createToolContext,
executeTool,
} from "./assistant-tools-timeline-shifts-test-helpers.js";
describe("assistant timeline allocation shift errors", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns stable allocation-not-found errors when timeline allocation persistence loses the row", async () => {
const existingAssignment = createExistingAssignment();
const missingDuringUpdate = {
code: "P2025",
message: "Record to update not found",
meta: { modelName: "Assignment" },
};
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: {
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 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.",
});
});
});
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionKey, SystemRole } from "@capakraken/shared";
import {
createExistingAssignment,
createToolContext,
executeTool,
} from "./assistant-tools-timeline-shifts-test-helpers.js";
describe("assistant timeline allocation shift tools", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("batch-shifts timeline allocations through the real timeline router mutation", async () => {
const existingAssignment = createExistingAssignment();
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" },
}),
);
});
});
@@ -0,0 +1,70 @@
import { AllocationStatus } from "@capakraken/shared";
import { vi } from "vitest";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
};
});
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 as executeAssistantTool } from "../router/assistant-tools.js";
export { createToolContext } from "./assistant-tools-advanced-timeline-test-helpers.js";
export const executeTool = executeAssistantTool;
export function createExistingAssignment() {
return {
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,
};
}