feat(platform): harden access scoping and delivery baseline

This commit is contained in:
2026-03-30 00:27:31 +02:00
parent 00b936fa1f
commit 819345acfa
109 changed files with 26142 additions and 8081 deletions
@@ -126,6 +126,15 @@ describe("assistant advanced tools and scoping", () => {
findUnique: vi
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: "project_lari",
name: "Gelddruckmaschine",
shortCode: "LARI",
status: "ACTIVE",
responsiblePerson: "Larissa Joos",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
})
.mockResolvedValueOnce({
id: "project_lari",
name: "Gelddruckmaschine",
@@ -228,6 +237,101 @@ describe("assistant advanced tools and scoping", () => {
);
});
it("returns project shift preview details from the canonical timeline router", async () => {
const projectFindUnique = vi.fn().mockImplementation((args: { where?: { id?: string; shortCode?: string }; select?: Record<string, unknown> }) => {
if (args.where?.id === "GDM") {
return Promise.resolve(null);
}
if (args.where?.shortCode === "GDM") {
return Promise.resolve({
id: "project_shift",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: "Larissa",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
});
}
if (args.select && "budgetCents" in args.select) {
return Promise.resolve({
id: "project_shift",
budgetCents: 100000,
winProbability: 100,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
});
}
return Promise.resolve({
id: "project_shift",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: "Larissa",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-16T00:00:00.000Z"),
});
});
const ctx = createToolContext(
{
project: {
findUnique: projectFindUnique,
findFirst: vi.fn(),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
},
[PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"preview_project_shift",
JSON.stringify({
projectIdentifier: "GDM",
newStartDate: "2026-01-19",
newEndDate: "2026-01-30",
}),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
project: {
id: "project_shift",
name: "Gelddruckmaschine",
shortCode: "GDM",
status: "ACTIVE",
responsiblePerson: "Larissa",
startDate: "2026-01-05",
endDate: "2026-01-16",
},
requestedShift: {
newStartDate: "2026-01-19",
newEndDate: "2026-01-30",
},
preview: {
valid: true,
errors: [],
warnings: [],
conflictDetails: [],
costImpact: {
currentTotalCents: 0,
newTotalCents: 0,
deltaCents: 0,
budgetCents: 100000,
budgetUtilizationBefore: 0,
budgetUtilizationAfter: 0,
wouldExceedBudget: false,
},
},
});
});
it("returns timeline entries view with demand, assignment, and holiday overlay context", async () => {
const ctx = createToolContext(
{
@@ -1248,9 +1352,94 @@ describe("assistant advanced tools and scoping", () => {
]));
});
it("scopes assistant notification listing to the current user", async () => {
it("returns a filtered project computation graph through the assistant", async () => {
const projectRecord = {
id: "project_1",
name: "Gelddruckmaschine",
shortCode: "GDM",
budgetCents: 100_000,
winProbability: 75,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-02-28T00:00:00.000Z"),
status: "ACTIVE",
responsiblePerson: "Larissa Joos",
};
const ctx = createToolContext(
{
project: {
findUnique: vi.fn().mockResolvedValue(projectRecord),
findFirst: vi.fn(),
findUniqueOrThrow: vi.fn().mockResolvedValue(projectRecord),
},
estimate: {
findFirst: vi.fn().mockResolvedValue(null),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
status: "CONFIRMED",
dailyCostCents: 4_000,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-30T00:00:00.000Z"),
hoursPerDay: 4,
},
]),
},
effortRule: {
count: vi.fn().mockResolvedValue(0),
},
experienceMultiplierRule: {
count: vi.fn().mockResolvedValue(0),
},
},
[PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
);
const result = await executeTool(
"get_project_computation_graph",
JSON.stringify({
projectId: "project_1",
domain: "BUDGET",
includeLinks: true,
}),
ctx,
);
const parsed = JSON.parse(result.content) as {
project: { id: string; shortCode: string; name: string };
requestedDomain: string;
totalNodeCount: number;
selectedNodeCount: number;
selectedLinkCount: number;
nodes: Array<{ id: string; domain: string }>;
links: Array<{ source: string; target: string }>;
meta: { projectName: string; projectCode: string };
};
expect(parsed.project).toEqual({
id: "project_1",
shortCode: "GDM",
name: "Gelddruckmaschine",
});
expect(parsed.meta).toEqual({
projectName: "Gelddruckmaschine",
projectCode: "GDM",
});
expect(parsed.requestedDomain).toBe("BUDGET");
expect(parsed.totalNodeCount).toBeGreaterThan(parsed.selectedNodeCount);
expect(parsed.selectedNodeCount).toBeGreaterThan(0);
expect(parsed.selectedLinkCount).toBeGreaterThan(0);
expect(parsed.nodes.every((node) => node.domain === "BUDGET")).toBe(true);
expect(parsed.links.length).toBe(parsed.selectedLinkCount);
});
it("scopes assistant notification listing to the current user through the router path", async () => {
const findMany = vi.fn().mockResolvedValue([]);
const ctx = createToolContext({
user: {
findUnique: vi.fn().mockResolvedValue({ id: "user_1" }),
},
notification: {
findMany,
},
@@ -1268,40 +1457,44 @@ describe("assistant advanced tools and scoping", () => {
);
});
it("rejects marking notifications that do not belong to the current user", async () => {
it("scopes mark_notification_read mutations to the current user through the router path", async () => {
const update = vi.fn();
const ctx = createToolContext({
user: {
findUnique: vi.fn().mockResolvedValue({ id: "user_1" }),
},
notification: {
findUnique: vi.fn().mockResolvedValue({ id: "notif_1", userId: "someone_else" }),
update,
},
});
const result = await executeTool(
await executeTool(
"mark_notification_read",
JSON.stringify({ notificationId: "notif_1" }),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: "Access denied: this notification does not belong to you",
expect(update).toHaveBeenCalledWith({
where: { id: "notif_1", userId: "user_1" },
data: expect.objectContaining({
readAt: expect.any(Date),
}),
});
expect(update).not.toHaveBeenCalled();
});
it("requires manageUsers before listing users through the assistant", async () => {
it("requires admin role before listing users through the assistant", async () => {
const findMany = vi.fn();
const ctx = createToolContext({
user: {
findMany,
},
});
}, [], SystemRole.MANAGER);
const result = await executeTool("list_users", JSON.stringify({ limit: 10 }), ctx);
expect(JSON.parse(result.content)).toEqual(
expect.objectContaining({
error: expect.stringContaining(PermissionKey.MANAGE_USERS),
error: expect.stringContaining("Admin role required"),
}),
);
expect(findMany).not.toHaveBeenCalled();