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"); }); });