Files
my-pi/pi-permission-system/test/scope-merge.test.ts

117 lines
4.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { MergedScopes } from "#src/scope-merge";
import { mergeScopesWithOrigins } from "#src/scope-merge";
describe("mergeScopesWithOrigins", () => {
it("returns empty result for empty scopes array", () => {
const result: MergedScopes = mergeScopesWithOrigins([]);
expect(result.mergedPermission).toEqual({});
expect(result.origins.size).toBe(0);
});
it("attributes a string surface value to the contributing scope via the '*' pattern", () => {
const result = mergeScopesWithOrigins([
["global", { permission: { bash: "allow" } }],
]);
expect(result.mergedPermission).toEqual({ bash: "allow" });
expect(result.origins.get("bash")?.get("*")).toBe("global");
});
it("attributes each pattern of an object surface value to the contributing scope", () => {
const result = mergeScopesWithOrigins([
[
"project",
{ permission: { bash: { "git *": "allow", "npm *": "deny" } } },
],
]);
expect(result.mergedPermission).toEqual({
bash: { "git *": "allow", "npm *": "deny" },
});
expect(result.origins.get("bash")?.get("git *")).toBe("project");
expect(result.origins.get("bash")?.get("npm *")).toBe("project");
});
it(
"shallow-merge: patterns not redefined by the higher scope keep their lower-scope origin;" +
" patterns the higher scope defines switch to the higher scope",
() => {
const result = mergeScopesWithOrigins([
[
"global",
{ permission: { bash: { "ls *": "allow", "git *": "allow" } } },
],
["project", { permission: { bash: { "git *": "deny" } } }],
]);
expect(result.mergedPermission).toEqual({
bash: { "ls *": "allow", "git *": "deny" },
});
// "ls *" was not touched by project — retains global attribution
expect(result.origins.get("bash")?.get("ls *")).toBe("global");
// "git *" was overridden by project — switches to project attribution
expect(result.origins.get("bash")?.get("git *")).toBe("project");
},
);
it("full replacement (string over object): higher scope re-attributes the entire surface to its own origin", () => {
const result = mergeScopesWithOrigins([
["global", { permission: { bash: { "ls *": "allow" } } }],
["project", { permission: { bash: "deny" } }],
]);
expect(result.mergedPermission).toEqual({ bash: "deny" });
// The string value produces a single "*" pattern for the replacing scope
expect(result.origins.get("bash")?.get("*")).toBe("project");
// The former "ls *" pattern from global is gone — origins are replaced, not merged
expect(result.origins.get("bash")?.has("ls *")).toBe(false);
});
it("full replacement (object over string): higher scope re-attributes the entire surface to its own origin", () => {
const result = mergeScopesWithOrigins([
["global", { permission: { bash: "ask" } }],
["project", { permission: { bash: { "git *": "deny" } } }],
]);
expect(result.mergedPermission).toEqual({ bash: { "git *": "deny" } });
// The object value attributes each pattern to the replacing scope
expect(result.origins.get("bash")?.get("git *")).toBe("project");
// The former "*" attribution from global is gone
expect(result.origins.get("bash")?.has("*")).toBe(false);
});
it("applies four-scope precedence in lowest→highest order (global → project → agent → project-agent)", () => {
const result = mergeScopesWithOrigins([
["global", { permission: { read: "ask" } }],
["project", { permission: { write: "deny" } }],
["agent", { permission: { bash: "deny" } }],
["project-agent", { permission: { mcp: "allow" } }],
]);
expect(result.mergedPermission).toEqual({
read: "ask",
write: "deny",
bash: "deny",
mcp: "allow",
});
expect(result.origins.get("read")?.get("*")).toBe("global");
expect(result.origins.get("write")?.get("*")).toBe("project");
expect(result.origins.get("bash")?.get("*")).toBe("agent");
expect(result.origins.get("mcp")?.get("*")).toBe("project-agent");
});
it("skips scopes with no permission key, contributing nothing to either map", () => {
const result = mergeScopesWithOrigins([
["global", {}],
["project", { permission: { bash: "allow" } }],
]);
expect(result.mergedPermission).toEqual({ bash: "allow" });
expect(result.origins.get("bash")?.get("*")).toBe("project");
});
it("attributes the universal '*' surface like any other (downstream reads origins.get('*')?.get('*') for universalFallbackOrigin)", () => {
const result = mergeScopesWithOrigins([
["global", { permission: { "*": "deny" } }],
["project", { permission: { "*": "allow" } }],
]);
expect(result.mergedPermission).toEqual({ "*": "allow" });
// Both scopes write a string — each is a full replacement; project wins last
expect(result.origins.get("*")?.get("*")).toBe("project");
});
});