test: CodeRabbit Generated Unit Tests: Add unit tests for permissions hooks, registry, and traversal (#23441)

* introduce a dependsOn permission string

* fix permissions toggle

* add DFS to allow permission chains

* Update packages/features/pbac/utils/permissionTraversal.ts

Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* fix splitPerms to work with attributes or dotted resources

* CodeRabbit Generated Unit Tests: Add unit tests for permissions hooks, registry, and traversal

* Fix UTG issues (iteration 1)

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-01-15 16:42:45 +00:00
1 parent 568d9de160
commit a328ff6bab
4 files changed
+633 -9

No files matched your search

@@ -0,0 +1,271 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PermissionLevel } from "./usePermissions";
vi.mock("@calcom/features/pbac/domain/types/permission-registry", () => {
const CrudAction = {
Create: "create",
Read: "read",
Update: "update",
Delete: "delete",
} as const;
const Scope = {
Team: "team",
Organization: "organization",
} as const;
const PERMISSION_REGISTRY = {
"*": { _resource: { i18nKey: "all" } },
calendar: {
[CrudAction.Create]: {},
[CrudAction.Read]: {},
[CrudAction.Update]: {},
[CrudAction.Delete]: {},
_resource: { i18nKey: "calendar" },
_internalMeta: true, // should be ignored
},
team: {
[CrudAction.Read]: {},
[CrudAction.Update]: {},
_resource: { i18nKey: "team" },
_secret: "ignore", // should be ignored
},
} as const;
// Mock getPermissionsForScope to return the registry without the "*" resource
const getPermissionsForScope = () => {
const { "*": _, ...rest } = PERMISSION_REGISTRY;
return rest;
};
return { CrudAction, Scope, PERMISSION_REGISTRY, getPermissionsForScope };
});
vi.mock("@calcom/features/pbac/utils/permissionTraversal", () => {
// Transitive graph (small):
// team.update -> depends on team.read
// calendar.delete -> depends on calendar.read
// Dependents (reverse):
// team.read -> dependents: team.update
// calendar.read -> dependents: calendar.delete
function getTransitiveDependencies(permission: string): string[] {
switch (permission) {
case "team.update":
return ["team.read"];
case "calendar.delete":
return ["calendar.read"];
default:
return [];
}
}
function getTransitiveDependents(permission: string): string[] {
switch (permission) {
case "team.read":
return ["team.update"];
case "calendar.read":
return ["calendar.delete"];
default:
return [];
}
}
return { getTransitiveDependencies, getTransitiveDependents };
});
// Import after mocks are set up
import { usePermissions } from "./usePermissions";
describe("usePermissions - permission utilities", () => {
let perms: ReturnType<typeof usePermissions>;
beforeEach(() => {
perms = usePermissions();
});
describe("hasAllPermissions", () => {
it("returns false when no permissions are granted", () => {
expect(perms.hasAllPermissions([])).toBe(false);
});
it("ignores internal keys and requires all non-internal actions across all resources", () => {
// Missing team.update
const some = ["calendar.create", "calendar.read", "calendar.update", "calendar.delete", "team.read"];
expect(perms.hasAllPermissions(some)).toBe(false);
const all = [
"calendar.create",
"calendar.read",
"calendar.update",
"calendar.delete",
"team.read",
"team.update",
];
expect(perms.hasAllPermissions(all)).toBe(true);
});
it("treats presence of *.* as sufficient only if individual perms also complete after modifications", () => {
// "*.*" by itself doesn't add individual entries; hasAllPermissions checks registry entries presence
expect(perms.hasAllPermissions(["*.*"])).toBe(false);
const fullPlusStar = [
"calendar.create",
"calendar.read",
"calendar.update",
"calendar.delete",
"team.read",
"team.update",
"*.*",
];
expect(perms.hasAllPermissions(fullPlusStar)).toBe(true);
});
});
describe("getResourcePermissionLevel", () => {
it("returns 'all' for '*' only when *.* is present", () => {
expect(perms.getResourcePermissionLevel("*", [])).toBe("none");
expect(perms.getResourcePermissionLevel("*", ["*.*"])).toBe("all");
});
it("returns 'none' for unknown resources even with wildcard", () => {
// Unknown resources return 'none' because they're not in the registry
// The wildcard check only applies to known resources
expect(perms.getResourcePermissionLevel("unknown", ["*.*"])).toBe("none");
expect(perms.getResourcePermissionLevel("unknown", [])).toBe("none");
});
it("returns 'all' when all non-internal actions for resource are granted", () => {
const list = ["calendar.create", "calendar.read", "calendar.update", "calendar.delete"];
expect(perms.getResourcePermissionLevel("calendar", list)).toBe("all");
});
it("returns 'read' when only read is present", () => {
expect(perms.getResourcePermissionLevel("team", ["team.read"])).toBe("read");
});
it("returns 'none' when no perms for that resource are present", () => {
expect(perms.getResourcePermissionLevel("team", ["calendar.read"])).toBe("none");
});
it("returns 'all' for any resource when wildcard exists", () => {
expect(perms.getResourcePermissionLevel("team", ["*.*"])).toBe("all");
expect(perms.getResourcePermissionLevel("calendar", ["*.*"])).toBe("all");
});
});
describe("toggleResourcePermissionLevel", () => {
it("for '*' with level 'all' adds '*.*' and all individual permissions", () => {
const next = perms.toggleResourcePermissionLevel("*", "all", []);
// Should include star and all concrete perms (6 in our registry)
expect(next).toEqual(
expect.arrayContaining([
"*.*",
"calendar.create",
"calendar.read",
"calendar.update",
"calendar.delete",
"team.read",
"team.update",
])
);
// No duplicates
expect(new Set(next).size).toBe(next.length);
});
it("for '*' with level 'none' removes '*.*' and leaves empty set", () => {
const next = perms.toggleResourcePermissionLevel("*", "none", ["*.*", "calendar.read", "team.read"]);
expect(next).not.toContain("*.*");
// Because we removed *.*, we don't auto-remove individuals unless resource-specific; keep others
expect(next).toEqual(expect.arrayContaining(["calendar.read", "team.read"]));
});
it("for specific resource with 'none' removes that resource's permissions only", () => {
const current = ["calendar.create", "calendar.read", "calendar.update", "team.read"];
const next = perms.toggleResourcePermissionLevel("calendar", "none", current);
expect(next).toEqual(["team.read"]);
});
it("for specific resource with 'read' keeps only read for that resource", () => {
const current = ["calendar.create", "calendar.read", "calendar.update", "team.read"];
const next = perms.toggleResourcePermissionLevel("calendar", "read", current);
// calendar.* collapsed to read only; team.read preserved
expect(next.sort()).toEqual(["calendar.read", "team.read"].sort());
});
it("for specific resource with 'all' expands to all non-internal actions for that resource", () => {
const current = ["team.read"];
const next = perms.toggleResourcePermissionLevel("calendar", "all", current);
expect(next).toEqual(
expect.arrayContaining([
"calendar.create",
"calendar.read",
"calendar.update",
"calendar.delete",
"team.read",
])
);
});
it("re-adds '*.*' when post-change state has all permissions across all resources", () => {
const almostAll = [
"calendar.create",
"calendar.read",
"calendar.update",
"calendar.delete",
"team.read",
];
const next = perms.toggleResourcePermissionLevel("team", "all", almostAll);
expect(next).toEqual(expect.arrayContaining(["*.*"]));
expect(perms.hasAllPermissions(next)).toBe(true);
});
it("no-ops and returns currentPermissions when resource not in registry", () => {
const curr = ["team.read"];
const next = perms.toggleResourcePermissionLevel("nonexistent", "all", curr);
// Implementation returns currentPermissions unchanged for unknown resource
expect(next).toBe(curr);
});
});
describe("toggleSinglePermission", () => {
it("enabling a permission adds it and its transitive dependencies, without duplicates", () => {
const next = perms.toggleSinglePermission("team.update", true, []);
expect(next).toEqual(expect.arrayContaining(["team.update", "team.read"]));
// unique
expect(new Set(next).size).toBe(next.length);
});
it("disabling a permission removes it and all its transitive dependents", () => {
const current = ["team.read", "team.update", "calendar.read"];
const next = perms.toggleSinglePermission("team.read", false, current);
// team.update depends on team.read, so it should also be removed
expect(next).toEqual(["calendar.read"]);
});
it("does not remove unrelated permissions when disabling one", () => {
const current = ["calendar.read", "team.read", "team.update"];
const next = perms.toggleSinglePermission("calendar.read", false, current);
// calendar.delete depends on read, so delete should be removed too if present; but it's not
expect(next.sort()).toEqual(["team.read", "team.update"].sort());
});
it("re-adds '*.*' when after toggling, all permissions are present", () => {
const allButStar = [
"calendar.create",
"calendar.read",
"calendar.update",
"calendar.delete",
"team.read",
];
const next = perms.toggleSinglePermission("team.update", true, allButStar);
expect(perms.hasAllPermissions(next)).toBe(true);
expect(next).toEqual(expect.arrayContaining(["*.*"]));
});
it("adding a permission for unknown resource still just adds/removes string tokens (since traversal returns none)", () => {
const start: string[] = [];
const add = perms.toggleSinglePermission("ghost.read", true, start);
expect(add).toEqual(["ghost.read"]);
const rem = perms.toggleSinglePermission("ghost.read", false, add);
expect(rem).toEqual([]);
});
});
});
@@ -0,0 +1,197 @@
/**
* Tests for permission registry utilities and data structure.
* Testing library/framework: This test suite is written to be compatible with Jest/Vitest style
* (describe/it/expect). It avoids framework-specific globals (like jest/vi) unless needed.
*/
import {
CrudAction,
CustomAction,
filterResourceConfig,
getPermissionsForScope,
PERMISSION_REGISTRY,
type PermissionRegistry,
parsePermissionString,
Resource,
type ResourceConfig,
Scope,
} from "./permission-registry";
describe("permission-registry: filterResourceConfig", () => {
it("removes the _resource property and preserves other keys", () => {
const cfg: ResourceConfig = {
_resource: { i18nKey: "foo" },
[CrudAction.Read]: {
description: "View",
category: "x",
i18nKey: "read",
descriptionI18nKey: "desc_read",
},
[CustomAction.Invite]: {
description: "Invite",
category: "x",
i18nKey: "invite",
descriptionI18nKey: "desc_invite",
},
};
const copy = JSON.parse(JSON.stringify(cfg));
const filtered = filterResourceConfig(cfg);
// does not mutate original
expect(cfg).toEqual(copy);
// _resource removed
expect(Object.hasOwn(filtered, "_resource")).toBe(false);
// remaining keys intact
expect(filtered).toHaveProperty(CrudAction.Read);
expect(filtered).toHaveProperty(CustomAction.Invite);
});
it("handles configs without _resource gracefully", () => {
const cfg: ResourceConfig = {
[CrudAction.Create]: {
description: "Create",
category: "x",
i18nKey: "create",
descriptionI18nKey: "desc_create",
},
};
const filtered = filterResourceConfig(cfg);
expect(filtered).toEqual(cfg);
});
it("returns empty object when only _resource is present", () => {
const cfg: ResourceConfig = {
_resource: { i18nKey: "only" },
};
const filtered = filterResourceConfig(cfg);
expect(filtered).toEqual({});
});
});
describe("permission-registry: PERMISSION_REGISTRY base structure", () => {
it("contains a _resource i18nKey for every resource", () => {
const registry: PermissionRegistry = PERMISSION_REGISTRY;
Object.entries(registry).forEach(([resource, cfg]) => {
expect(cfg).toBeDefined();
expect(cfg._resource).toBeDefined();
expect(typeof cfg._resource?.i18nKey).toBe("string");
expect(cfg._resource?.i18nKey.length).toBeGreaterThan(0);
});
});
it("all dependsOn permission strings point to existing resource.action entries", () => {
const registry: PermissionRegistry = PERMISSION_REGISTRY;
const hasPermission = (perm: string): boolean => {
// Use parsePermissionString to handle dotted resource names like "organization.attributes.read"
const { resource: resKey, action } = parsePermissionString(perm);
// In registry keys, Resource enums are their string values
const resourceConfig = registry[resKey as keyof PermissionRegistry] as ResourceConfig | undefined;
if (!resourceConfig) return false;
// Action might be CrudAction.All ("*") which is a valid key
return Object.hasOwn(resourceConfig, action);
};
Object.values(registry).forEach((cfg) => {
Object.entries(cfg).forEach(([action, details]) => {
if (action === "_resource") return;
const d = details as any;
if (Array.isArray(d.dependsOn)) {
d.dependsOn.forEach((dep: string) => {
expect(typeof dep).toBe("string");
expect(dep.includes(".")).toBe(true);
expect(hasPermission(dep)).toBe(true);
});
}
});
});
});
});
describe("permission-registry: getPermissionsForScope", () => {
it("Team scope excludes resources without team-eligible actions", () => {
const team = getPermissionsForScope(Scope.Team);
// "All" resource has only CrudAction.All with Organization scope -> should be omitted
expect(team[Resource.All]).toBeUndefined();
// Booking: Read has no scope (included), ReadTeamBookings has team scope (included),
// ReadOrgBookings has org scope (excluded)
expect(team[Resource.Booking]).toBeDefined();
const booking = team[Resource.Booking];
expect(booking).toBeDefined();
expect(booking[CrudAction.Read]).toBeDefined();
expect(booking[CustomAction.ReadTeamBookings]).toBeDefined();
expect(booking[CustomAction.ReadOrgBookings as any]).toBeUndefined();
// Team resource: Create has Organization scope -> excluded in Team scope
expect(team[Resource.Team]).toBeDefined();
expect(team[Resource.Team][CrudAction.Create]).toBeUndefined();
// But Read (no scope) is included
expect(team[Resource.Team][CrudAction.Read]).toBeDefined();
});
it("Organization scope includes org-specific and scope-unspecified actions", () => {
const org = getPermissionsForScope(Scope.Organization);
// "All" resource is org-only and should be present with * action
expect(org[Resource.All]).toBeDefined();
expect(org[Resource.All][CrudAction.All]).toBeDefined();
// Booking: Read (no scope) -> included, ReadOrgBookings (org) -> included, ReadTeamBookings (team) -> excluded
expect(org[Resource.Booking][CrudAction.Read]).toBeDefined();
expect(org[Resource.Booking][CustomAction.ReadOrgBookings]).toBeDefined();
expect(org[Resource.Booking][CustomAction.ReadTeamBookings as any]).toBeUndefined();
// Organization resource contains org-scoped actions
expect(org[Resource.Organization]).toBeDefined();
const orgRes = org[Resource.Organization];
expect(orgRes[CrudAction.Read]).toBeDefined();
expect(orgRes[CustomAction.ManageBilling]).toBeDefined();
});
it("resources with zero eligible actions for a scope are not included", () => {
// Construct a synthetic registry slice to validate "hasActions" logic indirectly
// but we will rely on real data: Resource.All should be absent in Team scope
const team = getPermissionsForScope(Scope.Team);
expect(team[Resource.All]).toBeUndefined();
});
it("backward compatibility: actions without scope appear in both Team and Organization results", () => {
const team = getPermissionsForScope(Scope.Team);
const org = getPermissionsForScope(Scope.Organization);
// Example: CrudAction.Read on Resource.Workflow has no scope -> present in both
expect(team[Resource.Workflow][CrudAction.Read]).toBeDefined();
expect(org[Resource.Workflow][CrudAction.Read]).toBeDefined();
});
});
describe("permission-registry: sanity for PermissionString format", () => {
it('each dependsOn entry follows "<resource>.<action>" format with valid enum values', () => {
const registry: PermissionRegistry = PERMISSION_REGISTRY;
const validResourceValues = new Set<string>(Object.values(Resource));
const validActionValues = new Set<string>([...Object.values(CrudAction), ...Object.values(CustomAction)]);
Object.values(registry).forEach((cfg) => {
Object.entries(cfg).forEach(([action, details]) => {
if (action === "_resource") return;
const d = details as any;
(d.dependsOn ?? []).forEach((dep: string) => {
// Use parsePermissionString to handle dotted resource names like "organization.attributes.read"
const { resource: res, action: act } = parsePermissionString(dep);
expect(validResourceValues.has(res)).toBe(true);
expect(validActionValues.has(act)).toBe(true);
});
});
});
});
});
@@ -322,17 +322,17 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
descriptionI18nKey: "pbac_desc_list_team_members",
dependsOn: ["team.read"],
visibleWhen: {
teamPrivacy: "public", // Only show for public teams
teamPrivacy: "public",
},
},
[CustomAction.ListMembersPrivate]: {
description: "List private team members",
category: "team",
i18nKey: "pbac_action_list_members", // Use same UI label as listMembers for consistency
descriptionI18nKey: "pbac_desc_list_team_members", // Use same description as listMembers
i18nKey: "pbac_action_list_members",
descriptionI18nKey: "pbac_desc_list_team_members",
dependsOn: ["team.read"],
visibleWhen: {
teamPrivacy: "private", // Only show for private teams
teamPrivacy: "private",
},
},
[CustomAction.ChangeMemberRole]: {
@@ -354,7 +354,7 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
category: "team",
i18nKey: "pbac_action_manage_billing",
descriptionI18nKey: "pbac_desc_manage_billing",
scope: [], // Empty scope because this permission is only used for TEAM billing outside of an org. (We dont want to show this in the UI)
scope: [],
},
},
[Resource.Organization]: {
@@ -383,18 +383,18 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
scope: [Scope.Organization],
dependsOn: ["organization.read"],
visibleWhen: {
teamPrivacy: "public", // Only show for public orgs
teamPrivacy: "public",
},
},
[CustomAction.ListMembersPrivate]: {
description: "List private organization members",
category: "org",
i18nKey: "pbac_action_list_members", // Same UI label as listMembers for consistency
descriptionI18nKey: "pbac_desc_list_organization_members", // Same description as listMembers
i18nKey: "pbac_action_list_members",
descriptionI18nKey: "pbac_desc_list_organization_members",
scope: [Scope.Organization],
dependsOn: ["organization.read"],
visibleWhen: {
teamPrivacy: "private", // Only show for private orgs
teamPrivacy: "private",
},
},
[CustomAction.Invite]: {
@@ -0,0 +1,156 @@
/**
* Unit tests for permission traversal utilities.
* Framework: This test file uses standard describe/it/expect APIs compatible with Jest and Vitest.
*/
import { describe, expect, it, vi } from "vitest";
let traversePermissions: (start: string, dir: "dependencies" | "dependents") => string[];
let getTransitiveDependencies: (p: string) => string[];
let getTransitiveDependents: (p: string) => string[];
vi.mock("../../domain/types/permission-registry", () => {
enum CrudAction {
Create = "create",
Read = "read",
Update = "update",
Delete = "delete",
}
enum Scope {
Team = "team",
Organization = "organization",
}
// Build a registry that lets us test a variety of scenarios
const PERMISSION_REGISTRY: Record<string, Record<string, any>> = {
post: {
_resource: { i18nKey: "post" },
[CrudAction.Read]: {
/* no explicit dependsOn */
},
[CrudAction.Create]: {
/* read dependency should be injected by back-compat */
},
[CrudAction.Update]: { dependsOn: ["category.read"] }, // explicit cross-resource dep
[CrudAction.Delete]: { dependsOn: ["post.update"] }, // forms a chain and back-compat adds read
publish: { dependsOn: ["post.update", "moderation.read"] } as any,
_internal: { note: "should be ignored" },
},
category: {
_resource: { i18nKey: "category" },
[CrudAction.Read]: {},
[CrudAction.Update]: { dependsOn: ["category.read"] },
},
moderation: {
_resource: { i18nKey: "moderation" },
[CrudAction.Read]: {},
archive: { dependsOn: ["post.publish"] }, // cycle across custom actions (publish -> moderation.read via depends; here reverse)
},
orphan: {
_resource: { i18nKey: "orphan" },
// Missing read; unusual resource to test unknown actions in traversal
ghost: { dependsOn: ["unknown.resource"] },
},
};
// Mock getPermissionsForScope to return the full registry for any scope
const getPermissionsForScope = () => PERMISSION_REGISTRY;
return { CrudAction, Scope, PERMISSION_REGISTRY, getPermissionsForScope };
});
// Now import the module under test, which will consume our mocked registry
import * as mod from "../permissionTraversal";
traversePermissions = mod.traversePermissions;
getTransitiveDependencies = mod.getTransitiveDependencies;
getTransitiveDependents = mod.getTransitiveDependents;
describe("permission traversal - dependencies", () => {
it("includes explicit dependsOn and applies CRUD read back-compat for create/update/delete", () => {
const depsCreate = traversePermissions("post.create", "dependencies").sort();
// Should include post.read (back-compat)
expect(depsCreate).toContain("post.read");
const depsUpdate = traversePermissions("post.update", "dependencies").sort();
// Explicit dependency + back-compat read for update
expect(depsUpdate).toEqual(expect.arrayContaining(["category.read", "post.read"]));
const depsDelete = traversePermissions("post.delete", "dependencies").sort();
// delete -> (explicit) post.update -> category.read and back-compat reads on both post.update and post.delete
expect(depsDelete).toEqual(expect.arrayContaining(["post.update", "post.read", "category.read"]));
});
it("traverses transitively across multiple levels without duplicates and excludes the start node", () => {
const depsPublish = traversePermissions("post.publish", "dependencies").sort();
// publish depends on post.update and moderation.read
// post.update depends on category.read and (back-compat) post.read
expect(depsPublish).toEqual(
expect.arrayContaining(["post.update", "moderation.read", "category.read", "post.read"])
);
// Start permission should not be included
expect(depsPublish).not.toContain("post.publish");
// No duplicates
const set = new Set(depsPublish);
expect(set.size).toBe(depsPublish.length);
});
it("handles cycles gracefully with visited set (no infinite loop)", () => {
// moderation.archive depends on post.publish, which depends on post.update -> ...
const depsArchive = traversePermissions("moderation.archive", "dependencies").sort();
// Ensure traversal completes and contains expected chain without repeating endlessly
expect(depsArchive).toEqual(
expect.arrayContaining(["post.publish", "post.update", "moderation.read", "category.read", "post.read"])
);
});
it("includes unknown resources/actions from dependsOn even if they don't exist in registry", () => {
const deps = traversePermissions("orphan.ghost", "dependencies");
// unknown.resource is listed in dependsOn, so it gets added to results
// even though it doesn't exist in the registry (the function doesn't validate existence)
expect(Array.isArray(deps)).toBe(true);
expect(deps).toContain("unknown.resource");
});
});
describe("permission traversal - dependents", () => {
it("includes dependents via explicit dependsOn references", () => {
// category.read is depended upon by post.update (explicit)
const dependents = traversePermissions("category.read", "dependents").sort();
expect(dependents).toContain("post.update");
});
it("includes CRUD dependents when current permission is read of the same resource", () => {
const dependents = traversePermissions("post.read", "dependents").sort();
// Any CRUD C/U/D of post should be included due to back-compat rule
expect(dependents).toEqual(expect.arrayContaining(["post.create", "post.update", "post.delete"]));
// Custom actions like publish are included transitively because post.publish depends on post.update
// which depends on post.read via the back-compat rule
expect(dependents).toContain("post.publish");
});
it("traverses dependents transitively and excludes the start node", () => {
// moderation.read is depended upon by post.publish; and post.publish is depended upon by moderation.archive
const dependents = traversePermissions("moderation.read", "dependents").sort();
expect(dependents).toEqual(expect.arrayContaining(["post.publish", "moderation.archive"]));
expect(dependents).not.toContain("moderation.read");
});
it("skips internal keys starting with underscore", () => {
const dependents = traversePermissions("post.read", "dependents");
// Ensure _internal doesn't appear as a dependent
expect(dependents.find((p) => p.includes("_internal"))).toBeUndefined();
});
});
describe("wrapper helpers", () => {
it("getTransitiveDependencies delegates correctly", () => {
const deps = getTransitiveDependencies("post.delete");
expect(deps).toEqual(expect.arrayContaining(["post.update", "post.read", "category.read"]));
});
it("getTransitiveDependents delegates correctly", () => {
const dependents = getTransitiveDependents("category.read");
expect(dependents).toContain("post.update");
});
});