test(api): cover assistant estimate reads

This commit is contained in:
2026-04-01 00:37:38 +02:00
parent c510eeae37
commit 80c31cc53f
5 changed files with 487 additions and 0 deletions
@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionKey, SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
approveEstimateVersion: vi.fn(),
cloneEstimate: vi.fn(),
createEstimateExport: vi.fn(),
createEstimatePlanningHandoff: vi.fn(),
createEstimateRevision: vi.fn(),
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
getEstimateById: vi.fn(),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
submitEstimateVersion: vi.fn(),
updateEstimateDraft: vi.fn(),
};
});
import { getEstimateById } from "@capakraken/application";
import { executeTool } from "../router/assistant-tools.js";
import {
createToolContext,
resetEstimateToolMocks,
} from "./assistant-tools-estimate-test-helpers.js";
describe("assistant estimate detail read tools", () => {
beforeEach(() => {
resetEstimateToolMocks();
});
it("reads estimate details through the real estimate router and rejects plain users", async () => {
vi.mocked(getEstimateById).mockResolvedValue({
id: "est_1",
name: "North Cluster Estimate",
status: "DRAFT",
versions: [],
} as Awaited<ReturnType<typeof getEstimateById>>);
const controllerCtx = createToolContext(
{},
{
userRole: SystemRole.CONTROLLER,
permissions: [PermissionKey.VIEW_COSTS],
},
);
const userCtx = createToolContext({}, { userRole: SystemRole.USER });
const successResult = await executeTool(
"get_estimate_detail",
JSON.stringify({ estimateId: "est_1" }),
controllerCtx,
);
const deniedResult = await executeTool(
"get_estimate_detail",
JSON.stringify({ estimateId: "est_1" }),
userCtx,
);
expect(vi.mocked(getEstimateById)).toHaveBeenCalledWith(controllerCtx.db, "est_1");
expect(JSON.parse(successResult.content)).toEqual(
expect.objectContaining({
id: "est_1",
name: "North Cluster Estimate",
}),
);
expect(JSON.parse(deniedResult.content)).toEqual(
expect.objectContaining({
error: "You do not have permission to perform this action.",
}),
);
});
});
@@ -0,0 +1,97 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionKey, SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
approveEstimateVersion: vi.fn(),
cloneEstimate: vi.fn(),
createEstimateExport: vi.fn(),
createEstimatePlanningHandoff: vi.fn(),
createEstimateRevision: vi.fn(),
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
getEstimateById: vi.fn(),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
submitEstimateVersion: vi.fn(),
updateEstimateDraft: vi.fn(),
};
});
import { getEstimateById } from "@capakraken/application";
import { executeTool } from "../router/assistant-tools.js";
import {
createToolContext,
resetEstimateToolMocks,
} from "./assistant-tools-estimate-test-helpers.js";
describe("assistant estimate read tools", () => {
beforeEach(() => {
resetEstimateToolMocks();
});
it("returns stable read errors for missing estimates and versions", async () => {
vi.mocked(getEstimateById).mockResolvedValueOnce(null as never);
const detailCtx = createToolContext(
{},
{
userRole: SystemRole.CONTROLLER,
permissions: [PermissionKey.VIEW_COSTS],
},
);
const detailResult = await executeTool(
"get_estimate_detail",
JSON.stringify({ estimateId: "missing_estimate" }),
detailCtx,
);
expect(JSON.parse(detailResult.content)).toEqual({
error: "Estimate not found with the given criteria.",
});
const versionsCtx = createToolContext(
{
estimate: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
{ userRole: SystemRole.CONTROLLER },
);
const versionsResult = await executeTool(
"list_estimate_versions",
JSON.stringify({ estimateId: "missing_estimate" }),
versionsCtx,
);
expect(JSON.parse(versionsResult.content)).toEqual({
error: "Estimate not found with the given criteria.",
});
const snapshotCtx = createToolContext(
{
estimate: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
{
userRole: SystemRole.CONTROLLER,
permissions: [PermissionKey.VIEW_COSTS],
},
);
const snapshotResult = await executeTool(
"get_estimate_version_snapshot",
JSON.stringify({ estimateId: "missing_estimate", versionId: "missing_version" }),
snapshotCtx,
);
expect(JSON.parse(snapshotResult.content)).toEqual({
error: "Estimate version not found with the given criteria.",
});
});
});
@@ -0,0 +1,160 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionKey, SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
approveEstimateVersion: vi.fn(),
cloneEstimate: vi.fn(),
createEstimateExport: vi.fn(),
createEstimatePlanningHandoff: vi.fn(),
createEstimateRevision: vi.fn(),
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
getEstimateById: vi.fn(),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
submitEstimateVersion: vi.fn(),
updateEstimateDraft: vi.fn(),
};
});
import { executeTool } from "../router/assistant-tools.js";
import {
createToolContext,
resetEstimateToolMocks,
} from "./assistant-tools-estimate-test-helpers.js";
describe("assistant estimate version snapshot tools", () => {
beforeEach(() => {
resetEstimateToolMocks();
});
it("reads estimate version snapshots through the real estimate router, requires viewCosts, and rejects plain users", async () => {
const db = {
estimate: {
findUnique: vi.fn().mockResolvedValue({
id: "est_1",
name: "North Cluster Estimate",
status: "APPROVED",
baseCurrency: "EUR",
versions: [
{
id: "ver_4",
versionNumber: 4,
label: "Rev 4",
status: "APPROVED",
notes: "Latest",
lockedAt: new Date("2026-03-29T00:00:00.000Z"),
createdAt: new Date("2026-03-28T00:00:00.000Z"),
updatedAt: new Date("2026-03-29T00:00:00.000Z"),
assumptions: [
{ id: "ass_1", category: "DELIVERY", key: "onsite", label: "Onsite support" },
],
scopeItems: [
{ id: "scope_1", scopeType: "EPIC", sequenceNo: 1, name: "Pipeline" },
],
demandLines: [
{
id: "dl_1",
name: "Modeling",
chapter: "3D",
hours: 40,
costTotalCents: 400000,
priceTotalCents: 600000,
currency: "EUR",
},
],
resourceSnapshots: [
{
id: "snap_1",
displayName: "Alice",
chapter: "3D",
currency: "EUR",
lcrCents: 10000,
ucrCents: 15000,
},
],
exports: [
{
id: "exp_1",
format: "XLSX",
fileName: "estimate.xlsx",
createdAt: new Date("2026-03-29T10:00:00.000Z"),
},
],
},
],
}),
},
};
const controllerCtx = createToolContext(db, {
userRole: SystemRole.CONTROLLER,
permissions: [PermissionKey.VIEW_COSTS],
});
const controllerWithoutCostsCtx = createToolContext(db, {
userRole: SystemRole.CONTROLLER,
permissions: [],
});
const userCtx = createToolContext({}, { userRole: SystemRole.USER });
const successResult = await executeTool(
"get_estimate_version_snapshot",
JSON.stringify({ estimateId: "est_1", versionId: "ver_4" }),
controllerCtx,
);
const missingPermissionResult = await executeTool(
"get_estimate_version_snapshot",
JSON.stringify({ estimateId: "est_1", versionId: "ver_4" }),
controllerWithoutCostsCtx,
);
const deniedResult = await executeTool(
"get_estimate_version_snapshot",
JSON.stringify({ estimateId: "est_1", versionId: "ver_4" }),
userCtx,
);
expect(db.estimate.findUnique).toHaveBeenCalledWith({
where: { id: "est_1" },
select: expect.objectContaining({
id: true,
versions: expect.objectContaining({
where: { id: "ver_4" },
}),
}),
});
expect(JSON.parse(successResult.content)).toEqual(
expect.objectContaining({
estimate: expect.objectContaining({
id: "est_1",
baseCurrency: "EUR",
}),
version: expect.objectContaining({
id: "ver_4",
versionNumber: 4,
}),
totals: expect.objectContaining({
hours: 40,
costTotalCents: 400000,
priceTotalCents: 600000,
}),
chapterBreakdown: [
expect.objectContaining({
chapter: "3D",
lineCount: 1,
}),
],
}),
);
expect(JSON.parse(missingPermissionResult.content)).toEqual(
expect.objectContaining({
error: expect.stringContaining(PermissionKey.VIEW_COSTS),
}),
);
expect(JSON.parse(deniedResult.content)).toEqual(
expect.objectContaining({
error: "You do not have permission to perform this action.",
}),
);
});
});
@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
approveEstimateVersion: vi.fn(),
cloneEstimate: vi.fn(),
createEstimateExport: vi.fn(),
createEstimatePlanningHandoff: vi.fn(),
createEstimateRevision: vi.fn(),
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
getEstimateById: vi.fn(),
listAssignmentBookings: vi.fn().mockResolvedValue([]),
submitEstimateVersion: vi.fn(),
updateEstimateDraft: vi.fn(),
};
});
import { executeTool } from "../router/assistant-tools.js";
import {
createToolContext,
resetEstimateToolMocks,
} from "./assistant-tools-estimate-test-helpers.js";
describe("assistant estimate version list tools", () => {
beforeEach(() => {
resetEstimateToolMocks();
});
it("lists estimate versions through the real estimate router and rejects plain users", async () => {
const db = {
estimate: {
findUnique: vi.fn().mockResolvedValue({
id: "est_1",
name: "North Cluster Estimate",
status: "DRAFT",
latestVersionNumber: 4,
versions: [
{
id: "ver_4",
versionNumber: 4,
label: "Rev 4",
status: "IN_REVIEW",
notes: "Latest",
lockedAt: null,
createdAt: new Date("2026-03-28T00:00:00.000Z"),
updatedAt: new Date("2026-03-29T00:00:00.000Z"),
_count: {
assumptions: 2,
scopeItems: 3,
demandLines: 4,
resourceSnapshots: 1,
exports: 0,
},
},
],
}),
},
};
const controllerCtx = createToolContext(db, { userRole: SystemRole.CONTROLLER });
const userCtx = createToolContext({}, { userRole: SystemRole.USER });
const successResult = await executeTool(
"list_estimate_versions",
JSON.stringify({ estimateId: "est_1" }),
controllerCtx,
);
const deniedResult = await executeTool(
"list_estimate_versions",
JSON.stringify({ estimateId: "est_1" }),
userCtx,
);
expect(db.estimate.findUnique).toHaveBeenCalledWith({
where: { id: "est_1" },
select: expect.objectContaining({
id: true,
versions: expect.any(Object),
}),
});
expect(JSON.parse(successResult.content)).toEqual(
expect.objectContaining({
id: "est_1",
latestVersionNumber: 4,
versions: [
expect.objectContaining({
id: "ver_4",
versionNumber: 4,
}),
],
}),
);
expect(JSON.parse(deniedResult.content)).toEqual(
expect.objectContaining({
error: "You do not have permission to perform this action.",
}),
);
});
});
@@ -0,0 +1,52 @@
import {
approveEstimateVersion,
cloneEstimate,
createEstimateExport,
createEstimatePlanningHandoff,
createEstimateRevision,
getEstimateById,
submitEstimateVersion,
updateEstimateDraft,
} from "@capakraken/application";
import { PermissionKey, SystemRole } from "@capakraken/shared";
import { vi } from "vitest";
import type { ToolContext } from "../router/assistant-tools.js";
export function createToolContext(
db: Record<string, unknown>,
options?: {
permissions?: PermissionKey[];
userRole?: SystemRole;
},
): ToolContext {
const userRole = options?.userRole ?? SystemRole.ADMIN;
return {
db: db as ToolContext["db"],
userId: "user_1",
userRole,
permissions: new Set(options?.permissions ?? []),
session: {
user: { email: "assistant@example.com", name: "Assistant User", image: null },
expires: "2026-03-29T00:00:00.000Z",
},
dbUser: {
id: "user_1",
systemRole: userRole,
permissionOverrides: null,
},
roleDefaults: null,
};
}
export function resetEstimateToolMocks() {
vi.clearAllMocks();
vi.mocked(approveEstimateVersion).mockReset();
vi.mocked(cloneEstimate).mockReset();
vi.mocked(createEstimateExport).mockReset();
vi.mocked(createEstimatePlanningHandoff).mockReset();
vi.mocked(createEstimateRevision).mockReset();
vi.mocked(getEstimateById).mockReset();
vi.mocked(submitEstimateVersion).mockReset();
vi.mocked(updateEstimateDraft).mockReset();
}