import { describe, expect, it } from "vitest"; import { normalizeSafeBins } from "./exec-approvals-allowlist.js"; import { makeMockCommandResolution, makeMockExecutableResolution, } from "./exec-approvals-test-helpers.js"; import { evaluateExecAllowlist, type ExecAllowlistEntry } from "./exec-approvals.js"; describe("exec approvals allowlist evaluation", () => { function evaluateAutoAllowSkills(params: { analysis: { ok: boolean; segments: Array<{ raw: string; argv: string[]; resolution: ReturnType; }>; }; resolvedPath: string; }) { return evaluateExecAllowlist({ analysis: params.analysis, allowlist: [], safeBins: new Set(), skillBins: [{ name: "skill-bin", resolvedPath: params.resolvedPath }], autoAllowSkills: true, cwd: "/tmp", }); } function expectAutoAllowSkillsMiss(result: ReturnType): void { expect(result.allowlistSatisfied).toBe(false); expect(result.segmentSatisfiedBy).toEqual([null]); } it("satisfies allowlist on exact match", () => { const analysis = { ok: true, segments: [ { raw: "tool", argv: ["tool"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "tool", resolvedPath: "/usr/bin/tool", executableName: "tool", }), }), }, ], }; const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/tool" }]; const result = evaluateExecAllowlist({ analysis, allowlist, safeBins: new Set(), cwd: "/tmp", }); expect(result.allowlistSatisfied).toBe(true); expect(result.allowlistMatches.map((entry) => entry.pattern)).toEqual(["/usr/bin/tool"]); }); it("satisfies allowlist via safe bins", () => { const analysis = { ok: true, segments: [ { raw: "jq .foo", argv: ["jq", ".foo"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "jq", resolvedPath: "/usr/bin/jq", executableName: "jq", }), }), }, ], }; const result = evaluateExecAllowlist({ analysis, allowlist: [], safeBins: normalizeSafeBins(["jq"]), cwd: "/tmp", }); // Safe bins are disabled on Windows (PowerShell parsing/expansion differences). if (process.platform === "win32") { expect(result.allowlistSatisfied).toBe(false); return; } expect(result.allowlistSatisfied).toBe(true); expect(result.allowlistMatches).toEqual([]); }); it("satisfies allowlist via auto-allow skills", () => { const analysis = { ok: true, segments: [ { raw: "skill-bin", argv: ["skill-bin", "--help"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "skill-bin", resolvedPath: "/opt/skills/skill-bin", executableName: "skill-bin", }), }), }, ], }; const result = evaluateAutoAllowSkills({ analysis, resolvedPath: "/opt/skills/skill-bin", }); expect(result.allowlistSatisfied).toBe(true); }); it("does not satisfy auto-allow skills for explicit relative paths", () => { const analysis = { ok: true, segments: [ { raw: "./skill-bin", argv: ["./skill-bin", "--help"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "./skill-bin", resolvedPath: "/tmp/skill-bin", executableName: "skill-bin", }), }), }, ], }; const result = evaluateAutoAllowSkills({ analysis, resolvedPath: "/tmp/skill-bin", }); expectAutoAllowSkillsMiss(result); }); it("does not satisfy auto-allow skills when command resolution is missing", () => { const analysis = { ok: true, segments: [ { raw: "skill-bin --help", argv: ["skill-bin", "--help"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "skill-bin", executableName: "skill-bin", }), }), }, ], }; const result = evaluateAutoAllowSkills({ analysis, resolvedPath: "/opt/skills/skill-bin", }); expectAutoAllowSkillsMiss(result); }); it("returns empty segment details for chain misses", () => { const segment = { raw: "tool", argv: ["tool"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "tool", resolvedPath: "/usr/bin/tool", executableName: "tool", }), }), }; const analysis = { ok: true, segments: [segment], chains: [[segment]], }; const result = evaluateExecAllowlist({ analysis, allowlist: [{ pattern: "/usr/bin/other" }], safeBins: new Set(), cwd: "/tmp", }); expect(result.allowlistSatisfied).toBe(false); expect(result.allowlistMatches).toEqual([]); expect(result.segmentSatisfiedBy).toEqual([]); }); it("aggregates segment satisfaction across chains", () => { const allowlistSegment = { raw: "tool", argv: ["tool"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "tool", resolvedPath: "/usr/bin/tool", executableName: "tool", }), }), }; const safeBinSegment = { raw: "jq .foo", argv: ["jq", ".foo"], resolution: makeMockCommandResolution({ execution: makeMockExecutableResolution({ rawExecutable: "jq", resolvedPath: "/usr/bin/jq", executableName: "jq", }), }), }; const analysis = { ok: true, segments: [allowlistSegment, safeBinSegment], chains: [[allowlistSegment], [safeBinSegment]], }; const result = evaluateExecAllowlist({ analysis, allowlist: [{ pattern: "/usr/bin/tool" }], safeBins: normalizeSafeBins(["jq"]), cwd: "/tmp", }); if (process.platform === "win32") { expect(result.allowlistSatisfied).toBe(false); return; } expect(result.allowlistSatisfied).toBe(true); expect(result.allowlistMatches.map((entry) => entry.pattern)).toEqual(["/usr/bin/tool"]); expect(result.segmentSatisfiedBy).toEqual(["allowlist", "safeBins"]); }); });