feat(platform): harden access scoping and delivery baseline
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PermissionKey, SystemRole, type PermissionKey as PermissionKeyValue } from "@capakraken/shared";
|
||||
import { apiRateLimiter } from "../middleware/rate-limit.js";
|
||||
import {
|
||||
ASSISTANT_CONFIRMATION_PREFIX,
|
||||
canExecuteMutationTool,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
getAvailableAssistantTools,
|
||||
listPendingAssistantApprovals,
|
||||
peekPendingAssistantApproval,
|
||||
selectAssistantToolsForRequest,
|
||||
} from "../router/assistant.js";
|
||||
import { TOOL_DEFINITIONS } from "../router/assistant-tools.js";
|
||||
|
||||
@@ -19,6 +21,19 @@ function getToolNames(
|
||||
return getAvailableAssistantTools(new Set(permissions), userRole).map((tool) => tool.function.name);
|
||||
}
|
||||
|
||||
function getSelectedToolNames(
|
||||
permissions: PermissionKeyValue[],
|
||||
messages: Array<{ role: "user" | "assistant"; content: string }>,
|
||||
userRole: SystemRole = SystemRole.ADMIN,
|
||||
pageContext?: string,
|
||||
) {
|
||||
return selectAssistantToolsForRequest(
|
||||
getAvailableAssistantTools(new Set(permissions), userRole),
|
||||
messages,
|
||||
pageContext,
|
||||
).map((tool) => tool.function.name);
|
||||
}
|
||||
|
||||
const TEST_USER_ID = "assistant-test-user";
|
||||
const TEST_CONVERSATION_ID = "assistant-test-conversation";
|
||||
|
||||
@@ -174,11 +189,22 @@ function createApprovalStoreMock() {
|
||||
};
|
||||
}
|
||||
|
||||
function createMissingApprovalTableError() {
|
||||
return Object.assign(
|
||||
new Error("The table `public.assistant_approvals` does not exist in the current database."),
|
||||
{
|
||||
code: "P2021",
|
||||
meta: { table: "public.assistant_approvals" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("assistant router tool gating", () => {
|
||||
let approvalStore = createApprovalStoreMock();
|
||||
|
||||
beforeEach(() => {
|
||||
approvalStore = createApprovalStoreMock();
|
||||
apiRateLimiter.reset();
|
||||
});
|
||||
|
||||
it("hides advanced tools unless the dedicated assistant permission is granted", () => {
|
||||
@@ -195,12 +221,115 @@ describe("assistant router tool gating", () => {
|
||||
expect(withAdvanced).toContain("get_project_computation_graph");
|
||||
});
|
||||
|
||||
it("keeps user administration tools behind manageUsers", () => {
|
||||
const withoutManageUsers = getToolNames([]);
|
||||
const withManageUsers = getToolNames([PermissionKey.MANAGE_USERS]);
|
||||
it("keeps user self-service tools available to plain authenticated users", () => {
|
||||
const userNames = getToolNames([], SystemRole.USER);
|
||||
|
||||
expect(withoutManageUsers).not.toContain("list_users");
|
||||
expect(withManageUsers).toContain("list_users");
|
||||
expect(userNames).toContain("get_current_user");
|
||||
expect(userNames).toContain("get_dashboard_layout");
|
||||
expect(userNames).toContain("save_dashboard_layout");
|
||||
expect(userNames).toContain("get_favorite_project_ids");
|
||||
expect(userNames).toContain("toggle_favorite_project");
|
||||
expect(userNames).toContain("get_column_preferences");
|
||||
expect(userNames).toContain("set_column_preferences");
|
||||
expect(userNames).toContain("get_mfa_status");
|
||||
expect(userNames).toContain("list_notifications");
|
||||
expect(userNames).toContain("get_unread_notification_count");
|
||||
expect(userNames).toContain("list_tasks");
|
||||
expect(userNames).toContain("get_task_counts");
|
||||
expect(userNames).toContain("create_reminder");
|
||||
expect(userNames).toContain("list_reminders");
|
||||
expect(userNames).toContain("update_reminder");
|
||||
expect(userNames).toContain("delete_reminder");
|
||||
});
|
||||
|
||||
it("keeps admin-only user tools hidden from non-admin roles", () => {
|
||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||
const userNames = getToolNames([], SystemRole.USER);
|
||||
|
||||
expect(adminNames).toContain("list_users");
|
||||
expect(adminNames).toContain("get_active_user_count");
|
||||
expect(adminNames).toContain("create_user");
|
||||
expect(adminNames).toContain("set_user_password");
|
||||
expect(adminNames).toContain("update_user_role");
|
||||
expect(adminNames).toContain("update_user_name");
|
||||
expect(adminNames).toContain("link_user_resource");
|
||||
expect(adminNames).toContain("auto_link_users_by_email");
|
||||
expect(adminNames).toContain("set_user_permissions");
|
||||
expect(adminNames).toContain("reset_user_permissions");
|
||||
expect(adminNames).toContain("get_effective_user_permissions");
|
||||
expect(adminNames).toContain("disable_user_totp");
|
||||
|
||||
expect(managerNames).not.toContain("list_users");
|
||||
expect(managerNames).not.toContain("create_user");
|
||||
expect(managerNames).not.toContain("set_user_permissions");
|
||||
expect(managerNames).not.toContain("disable_user_totp");
|
||||
expect(userNames).not.toContain("list_users");
|
||||
expect(userNames).not.toContain("get_active_user_count");
|
||||
expect(userNames).not.toContain("create_user");
|
||||
expect(userNames).not.toContain("set_user_password");
|
||||
expect(userNames).not.toContain("update_user_role");
|
||||
expect(userNames).not.toContain("update_user_name");
|
||||
expect(userNames).not.toContain("link_user_resource");
|
||||
expect(userNames).not.toContain("auto_link_users_by_email");
|
||||
expect(userNames).not.toContain("set_user_permissions");
|
||||
expect(userNames).not.toContain("reset_user_permissions");
|
||||
expect(userNames).not.toContain("get_effective_user_permissions");
|
||||
expect(userNames).not.toContain("disable_user_totp");
|
||||
});
|
||||
|
||||
it("caps the OpenAI tool payload to 128 definitions even for fully privileged admins", () => {
|
||||
const allPermissions = Object.values(PermissionKey);
|
||||
const selectedNames = getSelectedToolNames(
|
||||
allPermissions,
|
||||
[{ role: "user", content: "Bitte gib mir einen Überblick über das System." }],
|
||||
SystemRole.ADMIN,
|
||||
);
|
||||
|
||||
expect(selectedNames.length).toBeLessThanOrEqual(128);
|
||||
expect(selectedNames).toContain("get_current_user");
|
||||
expect(selectedNames).toContain("search_resources");
|
||||
expect(selectedNames).toContain("search_projects");
|
||||
});
|
||||
|
||||
it("prioritizes holiday and resource tools for German holiday questions", () => {
|
||||
const allPermissions = Object.values(PermissionKey);
|
||||
const selectedNames = getSelectedToolNames(
|
||||
allPermissions,
|
||||
[{ role: "user", content: "Kannst du mir alle Feiertage nennen, die Peter Parker in 2026 zustehen?" }],
|
||||
SystemRole.ADMIN,
|
||||
);
|
||||
|
||||
expect(selectedNames.length).toBeLessThanOrEqual(128);
|
||||
expect(selectedNames).toContain("search_resources");
|
||||
expect(selectedNames).toContain("get_resource");
|
||||
expect(selectedNames).toContain("get_resource_holidays");
|
||||
expect(selectedNames).toContain("list_holidays_by_region");
|
||||
expect(selectedNames).toContain("list_holiday_calendars");
|
||||
});
|
||||
|
||||
it("keeps assignable users and manager notification lifecycle tools behind manager/admin role", () => {
|
||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||
const userNames = getToolNames([], SystemRole.USER);
|
||||
|
||||
expect(managerNames).toContain("list_assignable_users");
|
||||
expect(managerNames).toContain("create_notification");
|
||||
expect(managerNames).toContain("create_task_for_user");
|
||||
expect(managerNames).toContain("assign_task");
|
||||
expect(managerNames).toContain("send_broadcast");
|
||||
expect(managerNames).toContain("list_broadcasts");
|
||||
expect(managerNames).toContain("get_broadcast_detail");
|
||||
expect(adminNames).toContain("list_assignable_users");
|
||||
expect(adminNames).toContain("create_task_for_user");
|
||||
expect(adminNames).toContain("send_broadcast");
|
||||
expect(userNames).not.toContain("list_assignable_users");
|
||||
expect(userNames).not.toContain("create_notification");
|
||||
expect(userNames).not.toContain("create_task_for_user");
|
||||
expect(userNames).not.toContain("assign_task");
|
||||
expect(userNames).not.toContain("send_broadcast");
|
||||
expect(userNames).not.toContain("list_broadcasts");
|
||||
expect(userNames).not.toContain("get_broadcast_detail");
|
||||
});
|
||||
|
||||
it("continues to hide cost-aware advanced tools when viewCosts is missing", () => {
|
||||
@@ -273,6 +402,66 @@ describe("assistant router tool gating", () => {
|
||||
expect(missingAdvancedNames).not.toContain("quick_assign_timeline_resource");
|
||||
});
|
||||
|
||||
it("keeps estimate lifecycle mutations behind manager/admin role and their router permissions", () => {
|
||||
const managerProjectNames = getToolNames([PermissionKey.MANAGE_PROJECTS], SystemRole.MANAGER);
|
||||
const managerAllocationNames = getToolNames([PermissionKey.MANAGE_ALLOCATIONS], SystemRole.MANAGER);
|
||||
const userProjectNames = getToolNames([PermissionKey.MANAGE_PROJECTS], SystemRole.USER);
|
||||
|
||||
expect(managerProjectNames).toContain("create_estimate");
|
||||
expect(managerProjectNames).toContain("clone_estimate");
|
||||
expect(managerProjectNames).toContain("update_estimate_draft");
|
||||
expect(managerProjectNames).toContain("submit_estimate_version");
|
||||
expect(managerProjectNames).toContain("approve_estimate_version");
|
||||
expect(managerProjectNames).toContain("create_estimate_revision");
|
||||
expect(managerProjectNames).toContain("create_estimate_export");
|
||||
expect(managerProjectNames).toContain("generate_estimate_weekly_phasing");
|
||||
expect(managerProjectNames).toContain("update_estimate_commercial_terms");
|
||||
expect(managerProjectNames).not.toContain("create_estimate_planning_handoff");
|
||||
expect(managerAllocationNames).toContain("create_estimate_planning_handoff");
|
||||
expect(managerAllocationNames).not.toContain("create_estimate");
|
||||
expect(userProjectNames).not.toContain("create_estimate");
|
||||
expect(userProjectNames).not.toContain("clone_estimate");
|
||||
expect(userProjectNames).not.toContain("update_estimate_draft");
|
||||
expect(userProjectNames).not.toContain("submit_estimate_version");
|
||||
expect(userProjectNames).not.toContain("approve_estimate_version");
|
||||
expect(userProjectNames).not.toContain("create_estimate_revision");
|
||||
expect(userProjectNames).not.toContain("create_estimate_export");
|
||||
expect(userProjectNames).not.toContain("generate_estimate_weekly_phasing");
|
||||
expect(userProjectNames).not.toContain("update_estimate_commercial_terms");
|
||||
expect(userProjectNames).not.toContain("create_estimate_planning_handoff");
|
||||
});
|
||||
|
||||
it("keeps estimate read tools aligned to controller/manager/admin visibility and cost requirements", () => {
|
||||
const controllerNames = getToolNames([PermissionKey.VIEW_COSTS], SystemRole.CONTROLLER);
|
||||
const controllerWithoutCosts = getToolNames([], SystemRole.CONTROLLER);
|
||||
const managerNames = getToolNames([PermissionKey.VIEW_COSTS], SystemRole.MANAGER);
|
||||
const managerWithoutCosts = getToolNames([], SystemRole.MANAGER);
|
||||
const userNames = getToolNames([PermissionKey.VIEW_COSTS], SystemRole.USER);
|
||||
|
||||
expect(controllerNames).toContain("get_estimate_detail");
|
||||
expect(controllerNames).toContain("list_estimate_versions");
|
||||
expect(controllerNames).toContain("get_estimate_version_snapshot");
|
||||
expect(controllerNames).toContain("get_estimate_weekly_phasing");
|
||||
expect(controllerNames).toContain("get_estimate_commercial_terms");
|
||||
expect(controllerWithoutCosts).not.toContain("get_estimate_detail");
|
||||
expect(controllerWithoutCosts).toContain("list_estimate_versions");
|
||||
expect(controllerWithoutCosts).not.toContain("get_estimate_version_snapshot");
|
||||
expect(controllerWithoutCosts).toContain("get_estimate_weekly_phasing");
|
||||
expect(controllerWithoutCosts).toContain("get_estimate_commercial_terms");
|
||||
expect(managerNames).toContain("get_estimate_detail");
|
||||
expect(managerNames).toContain("list_estimate_versions");
|
||||
expect(managerNames).toContain("get_estimate_version_snapshot");
|
||||
expect(managerNames).toContain("get_estimate_weekly_phasing");
|
||||
expect(managerNames).toContain("get_estimate_commercial_terms");
|
||||
expect(managerWithoutCosts).toContain("list_estimate_versions");
|
||||
expect(managerWithoutCosts).not.toContain("get_estimate_version_snapshot");
|
||||
expect(userNames).not.toContain("get_estimate_detail");
|
||||
expect(userNames).not.toContain("list_estimate_versions");
|
||||
expect(userNames).not.toContain("get_estimate_version_snapshot");
|
||||
expect(userNames).not.toContain("get_estimate_weekly_phasing");
|
||||
expect(userNames).not.toContain("get_estimate_commercial_terms");
|
||||
});
|
||||
|
||||
it("keeps import/dispo parity tools aligned to router roles and permissions", () => {
|
||||
const managerNames = getToolNames([PermissionKey.IMPORT_DATA], SystemRole.MANAGER);
|
||||
const controllerNames = getToolNames([], SystemRole.CONTROLLER);
|
||||
@@ -284,11 +473,54 @@ describe("assistant router tool gating", () => {
|
||||
expect(controllerNames).toContain("export_projects_csv");
|
||||
expect(adminNames).toContain("list_dispo_import_batches");
|
||||
expect(adminNames).toContain("get_dispo_import_batch");
|
||||
expect(adminNames).toContain("stage_dispo_import_batch");
|
||||
expect(adminNames).toContain("validate_dispo_import_batch");
|
||||
expect(adminNames).toContain("cancel_dispo_import_batch");
|
||||
expect(adminNames).toContain("list_dispo_staged_resources");
|
||||
expect(adminNames).toContain("list_dispo_staged_projects");
|
||||
expect(adminNames).toContain("list_dispo_staged_assignments");
|
||||
expect(adminNames).toContain("list_dispo_staged_vacations");
|
||||
expect(adminNames).toContain("list_dispo_staged_unresolved_records");
|
||||
expect(adminNames).toContain("resolve_dispo_staged_record");
|
||||
expect(adminNames).toContain("commit_dispo_import_batch");
|
||||
expect(userNames).not.toContain("import_csv_data");
|
||||
expect(userNames).not.toContain("export_resources_csv");
|
||||
expect(userNames).not.toContain("export_projects_csv");
|
||||
expect(userNames).not.toContain("list_dispo_import_batches");
|
||||
expect(userNames).not.toContain("get_dispo_import_batch");
|
||||
expect(userNames).not.toContain("stage_dispo_import_batch");
|
||||
expect(userNames).not.toContain("validate_dispo_import_batch");
|
||||
expect(userNames).not.toContain("list_dispo_staged_resources");
|
||||
expect(userNames).not.toContain("commit_dispo_import_batch");
|
||||
});
|
||||
|
||||
it("keeps settings and webhook admin tools hidden while preserving protected parity tools", () => {
|
||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||
const userNames = getToolNames([], SystemRole.USER);
|
||||
|
||||
expect(adminNames).toContain("get_system_settings");
|
||||
expect(adminNames).toContain("update_system_settings");
|
||||
expect(adminNames).toContain("test_ai_connection");
|
||||
expect(adminNames).toContain("test_smtp_connection");
|
||||
expect(adminNames).toContain("test_gemini_connection");
|
||||
expect(adminNames).toContain("update_system_role_config");
|
||||
expect(adminNames).toContain("list_webhooks");
|
||||
expect(adminNames).toContain("get_webhook");
|
||||
expect(adminNames).toContain("create_webhook");
|
||||
expect(adminNames).toContain("update_webhook");
|
||||
expect(adminNames).toContain("delete_webhook");
|
||||
expect(adminNames).toContain("test_webhook");
|
||||
expect(adminNames).toContain("get_ai_configured");
|
||||
expect(adminNames).toContain("list_system_role_configs");
|
||||
|
||||
expect(userNames).not.toContain("get_system_settings");
|
||||
expect(userNames).not.toContain("update_system_settings");
|
||||
expect(userNames).not.toContain("test_ai_connection");
|
||||
expect(userNames).not.toContain("update_system_role_config");
|
||||
expect(userNames).not.toContain("list_webhooks");
|
||||
expect(userNames).not.toContain("create_webhook");
|
||||
expect(userNames).toContain("get_ai_configured");
|
||||
expect(userNames).toContain("list_system_role_configs");
|
||||
});
|
||||
|
||||
it("keeps holiday calendar mutation tools admin-only while leaving read tools available", () => {
|
||||
@@ -506,6 +738,59 @@ describe("assistant router tool gating", () => {
|
||||
expect(approvalSummaries).not.toContain("Foreign");
|
||||
});
|
||||
|
||||
it("degrades approval reads gracefully when approval storage is missing", async () => {
|
||||
const missingTableError = createMissingApprovalTableError();
|
||||
const missingStore = {
|
||||
assistantApproval: {
|
||||
findFirst: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
findMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
create: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
updateMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(listPendingAssistantApprovals(missingStore, TEST_USER_ID)).resolves.toEqual([]);
|
||||
await expect(peekPendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
await expect(consumePendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
await expect(clearPendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns an explicit error when approval storage is missing for mutation confirmation", async () => {
|
||||
const missingTableError = createMissingApprovalTableError();
|
||||
const missingStore = {
|
||||
assistantApproval: {
|
||||
findFirst: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
findMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
create: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
updateMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(createPendingAssistantApproval(
|
||||
missingStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Apollo" }),
|
||||
)).rejects.toThrow("Assistant approval storage is unavailable");
|
||||
});
|
||||
|
||||
it("does not require confirmation for read-only assistant tools", () => {
|
||||
expect(canExecuteMutationTool([
|
||||
{ role: "user", content: "Zeig mir meine Notifications" },
|
||||
@@ -518,12 +803,31 @@ describe("assistant router tool gating", () => {
|
||||
);
|
||||
|
||||
expect(toolDescriptions.get("create_estimate")).toContain("manageProjects");
|
||||
expect(toolDescriptions.get("set_entitlement")).toContain("manageVacations");
|
||||
expect(toolDescriptions.get("create_org_unit")).toContain("manageResources");
|
||||
expect(toolDescriptions.get("update_org_unit")).toContain("manageResources");
|
||||
expect(toolDescriptions.get("list_users")).toContain("manageUsers");
|
||||
expect(toolDescriptions.get("create_task_for_user")).toContain("manageProjects");
|
||||
expect(toolDescriptions.get("send_broadcast")).toContain("manageProjects");
|
||||
expect(toolDescriptions.get("create_estimate_planning_handoff")).toContain("manageAllocations");
|
||||
expect(toolDescriptions.get("get_estimate_detail")).toContain("Controller/manager/admin");
|
||||
expect(toolDescriptions.get("list_estimate_versions")).toContain("Controller/manager/admin");
|
||||
expect(toolDescriptions.get("get_estimate_version_snapshot")).toContain("viewCosts");
|
||||
expect(toolDescriptions.get("get_estimate_weekly_phasing")).toContain("Controller/manager/admin");
|
||||
expect(toolDescriptions.get("get_estimate_commercial_terms")).toContain("Controller/manager/admin");
|
||||
expect(toolDescriptions.get("create_vacation")).toContain("authenticated user");
|
||||
expect(toolDescriptions.get("approve_vacation")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("reject_vacation")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("cancel_vacation")).toContain("Users can cancel their own requests");
|
||||
expect(toolDescriptions.get("set_entitlement")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("create_role")).toContain("manageRoles");
|
||||
expect(toolDescriptions.get("update_role")).toContain("manageRoles");
|
||||
expect(toolDescriptions.get("delete_role")).toContain("manageRoles");
|
||||
expect(toolDescriptions.get("create_org_unit")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("update_org_unit")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("list_users")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("list_assignable_users")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("get_current_user")).toContain("authenticated user's own profile");
|
||||
expect(toolDescriptions.get("create_notification")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("create_task_for_user")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("send_broadcast")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("get_broadcast_detail")).toContain("Manager or admin role");
|
||||
expect(toolDescriptions.get("create_client")).toContain("manager or admin role");
|
||||
expect(toolDescriptions.get("update_client")).toContain("manager or admin role");
|
||||
expect(toolDescriptions.get("create_holiday_calendar")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("create_holiday_calendar_entry")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("query_change_history")).toContain("Controller/manager/admin");
|
||||
@@ -534,6 +838,17 @@ describe("assistant router tool gating", () => {
|
||||
expect(toolDescriptions.get("import_csv_data")).toContain("manager/admin");
|
||||
expect(toolDescriptions.get("list_dispo_import_batches")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("get_dispo_import_batch")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("stage_dispo_import_batch")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("validate_dispo_import_batch")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("commit_dispo_import_batch")).toContain("Always confirm first");
|
||||
expect(toolDescriptions.get("get_system_settings")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("update_system_settings")).toContain("Always confirm first");
|
||||
expect(toolDescriptions.get("get_ai_configured")).toContain("authenticated user");
|
||||
expect(toolDescriptions.get("list_system_role_configs")).toContain("authenticated user");
|
||||
expect(toolDescriptions.get("update_system_role_config")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("list_webhooks")).toContain("Secrets are masked");
|
||||
expect(toolDescriptions.get("create_webhook")).toContain("Always confirm first");
|
||||
expect(toolDescriptions.get("test_webhook")).toContain("Always confirm first");
|
||||
expect(toolDescriptions.get("list_audit_log_entries")).toContain("Controller/manager/admin");
|
||||
expect(toolDescriptions.get("get_audit_log_entry")).toContain("Controller/manager/admin");
|
||||
expect(toolDescriptions.get("get_audit_log_timeline")).toContain("Controller/manager/admin");
|
||||
@@ -548,4 +863,72 @@ describe("assistant router tool gating", () => {
|
||||
expect(toolDescriptions.get("batch_quick_assign_timeline_resources")).toContain("manageAllocations");
|
||||
expect(toolDescriptions.get("batch_shift_timeline_allocations")).toContain("manager/admin");
|
||||
});
|
||||
|
||||
it("aligns assistant tool visibility with router role and permission rules", () => {
|
||||
const managerWithRolePermission = getToolNames(
|
||||
[PermissionKey.MANAGE_ROLES],
|
||||
SystemRole.MANAGER,
|
||||
);
|
||||
const managerWithoutRolePermission = getToolNames([], SystemRole.MANAGER);
|
||||
|
||||
expect(managerWithRolePermission).toContain("create_role");
|
||||
expect(managerWithRolePermission).toContain("update_role");
|
||||
expect(managerWithRolePermission).toContain("delete_role");
|
||||
expect(managerWithRolePermission).toContain("create_client");
|
||||
expect(managerWithRolePermission).toContain("update_client");
|
||||
expect(managerWithRolePermission).not.toContain("create_org_unit");
|
||||
expect(managerWithRolePermission).not.toContain("update_org_unit");
|
||||
|
||||
expect(managerWithoutRolePermission).not.toContain("create_role");
|
||||
expect(managerWithoutRolePermission).not.toContain("update_role");
|
||||
expect(managerWithoutRolePermission).not.toContain("delete_role");
|
||||
expect(managerWithoutRolePermission).toContain("create_client");
|
||||
expect(managerWithoutRolePermission).toContain("update_client");
|
||||
|
||||
const adminWithRolePermission = getToolNames(
|
||||
[PermissionKey.MANAGE_ROLES],
|
||||
SystemRole.ADMIN,
|
||||
);
|
||||
expect(adminWithRolePermission).toContain("create_org_unit");
|
||||
expect(adminWithRolePermission).toContain("update_org_unit");
|
||||
|
||||
const standardUserTools = getToolNames([], SystemRole.USER);
|
||||
expect(standardUserTools).toContain("get_vacation_balance");
|
||||
expect(standardUserTools).toContain("create_vacation");
|
||||
expect(standardUserTools).toContain("cancel_vacation");
|
||||
expect(standardUserTools).not.toContain("approve_vacation");
|
||||
expect(standardUserTools).not.toContain("reject_vacation");
|
||||
expect(standardUserTools).not.toContain("set_entitlement");
|
||||
|
||||
const managerVacationTools = getToolNames([], SystemRole.MANAGER);
|
||||
expect(managerVacationTools).toContain("approve_vacation");
|
||||
expect(managerVacationTools).toContain("reject_vacation");
|
||||
expect(managerVacationTools).toContain("set_entitlement");
|
||||
});
|
||||
|
||||
it("keeps estimate tool parameter enums aligned with the current estimate schema", () => {
|
||||
const definitionByName = new Map(
|
||||
TOOL_DEFINITIONS.map((tool) => [tool.function.name, tool.function]),
|
||||
);
|
||||
|
||||
const createEstimateStatus = (
|
||||
definitionByName.get("create_estimate")?.parameters as {
|
||||
properties?: Record<string, { enum?: unknown[] }>;
|
||||
}
|
||||
)?.properties?.status?.enum;
|
||||
const updateEstimateStatus = (
|
||||
definitionByName.get("update_estimate_draft")?.parameters as {
|
||||
properties?: Record<string, { enum?: unknown[] }>;
|
||||
}
|
||||
)?.properties?.status?.enum;
|
||||
const estimateExportFormats = (
|
||||
definitionByName.get("create_estimate_export")?.parameters as {
|
||||
properties?: Record<string, { enum?: unknown[] }>;
|
||||
}
|
||||
)?.properties?.format?.enum;
|
||||
|
||||
expect(createEstimateStatus).toEqual(["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"]);
|
||||
expect(updateEstimateStatus).toEqual(["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"]);
|
||||
expect(estimateExportFormats).toEqual(["XLSX", "CSV", "JSON", "SAP", "MMP"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user