import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { makePathEnv, makeTempDir } from "./exec-approvals-test-helpers.js"; import { analyzeArgvCommand, analyzeShellCommand, buildEnforcedShellCommand, buildSafeBinsShellCommand, evaluateExecAllowlist, evaluateShellAllowlist, matchAllowlist, maxAsk, mergeExecApprovalsSocketDefaults, minSecurity, normalizeExecApprovals, parseExecArgvToken, normalizeSafeBins, requiresExecApproval, resolveCommandResolution, resolveCommandResolutionFromArgv, resolveExecApprovalsPath, resolveExecApprovalsSocketPath, type ExecAllowlistEntry, } from "./exec-approvals.js"; function buildNestedEnvShellCommand(params: { envExecutable: string; depth: number; payload: string; }): string[] { return [...Array(params.depth).fill(params.envExecutable), "/bin/sh", "-c", params.payload]; } function analyzeEnvWrapperAllowlist(params: { argv: string[]; envPath: string; cwd: string }) { const analysis = analyzeArgvCommand({ argv: params.argv, cwd: params.cwd, env: makePathEnv(params.envPath), }); const allowlistEval = evaluateExecAllowlist({ analysis, allowlist: [{ pattern: params.envPath }], safeBins: normalizeSafeBins([]), cwd: params.cwd, }); return { analysis, allowlistEval }; } function createPathExecutableFixture(params?: { executable?: string }): { exeName: string; exePath: string; binDir: string; } { const dir = makeTempDir(); const binDir = path.join(dir, "bin"); fs.mkdirSync(binDir, { recursive: true }); const baseName = params?.executable ?? "rg"; const exeName = process.platform === "win32" ? `${baseName}.exe` : baseName; const exePath = path.join(binDir, exeName); fs.writeFileSync(exePath, ""); fs.chmodSync(exePath, 0o755); return { exeName, exePath, binDir }; } describe("exec approvals allowlist matching", () => { const baseResolution = { rawExecutable: "rg", resolvedPath: "/opt/homebrew/bin/rg", executableName: "rg", }; it("handles wildcard/path matching semantics", () => { const cases: Array<{ entries: ExecAllowlistEntry[]; expectedPattern: string | null }> = [ { entries: [{ pattern: "RG" }], expectedPattern: null }, { entries: [{ pattern: "/opt/**/rg" }], expectedPattern: "/opt/**/rg" }, { entries: [{ pattern: "/opt/*/rg" }], expectedPattern: null }, ]; for (const testCase of cases) { const match = matchAllowlist(testCase.entries, baseResolution); expect(match?.pattern ?? null).toBe(testCase.expectedPattern); } }); it("matches bare * wildcard pattern against any resolved path", () => { const match = matchAllowlist([{ pattern: "*" }], baseResolution); expect(match).not.toBeNull(); expect(match?.pattern).toBe("*"); }); it("matches bare * wildcard against arbitrary executables", () => { const match = matchAllowlist([{ pattern: "*" }], { rawExecutable: "python3", resolvedPath: "/usr/bin/python3", executableName: "python3", }); expect(match).not.toBeNull(); expect(match?.pattern).toBe("*"); }); it("matches absolute paths containing regex metacharacters", () => { const plusPathCases = ["/usr/bin/g++", "/usr/bin/clang++"]; for (const candidatePath of plusPathCases) { const match = matchAllowlist([{ pattern: candidatePath }], { rawExecutable: candidatePath, resolvedPath: candidatePath, executableName: candidatePath.split("/").at(-1) ?? candidatePath, }); expect(match?.pattern).toBe(candidatePath); } }); it("does not throw when wildcard globs are mixed with + in path", () => { const match = matchAllowlist([{ pattern: "/usr/bin/*++" }], { rawExecutable: "/usr/bin/g++", resolvedPath: "/usr/bin/g++", executableName: "g++", }); expect(match?.pattern).toBe("/usr/bin/*++"); }); it("matches paths containing []() regex tokens literally", () => { const literalPattern = "/opt/builds/tool[1](stable)"; const match = matchAllowlist([{ pattern: literalPattern }], { rawExecutable: literalPattern, resolvedPath: literalPattern, executableName: "tool[1](stable)", }); expect(match?.pattern).toBe(literalPattern); }); }); describe("mergeExecApprovalsSocketDefaults", () => { it("prefers normalized socket, then current, then default path", () => { const normalized = normalizeExecApprovals({ version: 1, agents: {}, socket: { path: "/tmp/a.sock", token: "a" }, }); const current = normalizeExecApprovals({ version: 1, agents: {}, socket: { path: "/tmp/b.sock", token: "b" }, }); const merged = mergeExecApprovalsSocketDefaults({ normalized, current }); expect(merged.socket?.path).toBe("/tmp/a.sock"); expect(merged.socket?.token).toBe("a"); }); it("falls back to current token when missing in normalized", () => { const normalized = normalizeExecApprovals({ version: 1, agents: {} }); const current = normalizeExecApprovals({ version: 1, agents: {}, socket: { path: "/tmp/b.sock", token: "b" }, }); const merged = mergeExecApprovalsSocketDefaults({ normalized, current }); expect(merged.socket?.path).toBeTruthy(); expect(merged.socket?.token).toBe("b"); }); }); describe("resolve exec approvals defaults", () => { it("expands home-prefixed default file and socket paths", () => { const dir = makeTempDir(); const prevOpenClawHome = process.env.OPENCLAW_HOME; try { process.env.OPENCLAW_HOME = dir; expect(path.normalize(resolveExecApprovalsPath())).toBe( path.normalize(path.join(dir, ".openclaw", "exec-approvals.json")), ); expect(path.normalize(resolveExecApprovalsSocketPath())).toBe( path.normalize(path.join(dir, ".openclaw", "exec-approvals.sock")), ); } finally { if (prevOpenClawHome === undefined) { delete process.env.OPENCLAW_HOME; } else { process.env.OPENCLAW_HOME = prevOpenClawHome; } } }); }); describe("exec approvals safe shell command builder", () => { it("quotes only safeBins segments (leaves other segments untouched)", () => { if (process.platform === "win32") { return; } const analysis = analyzeShellCommand({ command: "rg foo src/*.ts | head -n 5 && echo ok", cwd: "/tmp", env: { PATH: "/usr/bin:/bin" }, platform: process.platform, }); expect(analysis.ok).toBe(true); const res = buildSafeBinsShellCommand({ command: "rg foo src/*.ts | head -n 5 && echo ok", segments: analysis.segments, segmentSatisfiedBy: [null, "safeBins", null], platform: process.platform, }); expect(res.ok).toBe(true); // Preserve non-safeBins segment raw (glob stays unquoted) expect(res.command).toContain("rg foo src/*.ts"); // SafeBins segment is fully quoted and pinned to its resolved absolute path. expect(res.command).toMatch(/'[^']*\/head' '-n' '5'/); }); it("enforces canonical planned argv for every approved segment", () => { if (process.platform === "win32") { return; } const analysis = analyzeShellCommand({ command: "env rg -n needle", cwd: "/tmp", env: { PATH: "/usr/bin:/bin" }, platform: process.platform, }); expect(analysis.ok).toBe(true); const res = buildEnforcedShellCommand({ command: "env rg -n needle", segments: analysis.segments, platform: process.platform, }); expect(res.ok).toBe(true); expect(res.command).toMatch(/'(?:[^']*\/)?rg' '-n' 'needle'/); expect(res.command).not.toContain("'env'"); }); }); describe("exec approvals command resolution", () => { it("resolves PATH, relative, and quoted executables", () => { const cases = [ { name: "PATH executable", setup: () => { const fixture = createPathExecutableFixture(); return { command: "rg -n foo", cwd: undefined as string | undefined, envPath: makePathEnv(fixture.binDir), expectedPath: fixture.exePath, expectedExecutableName: fixture.exeName, }; }, }, { name: "relative executable", setup: () => { const dir = makeTempDir(); const cwd = path.join(dir, "project"); const script = path.join(cwd, "scripts", "run.sh"); fs.mkdirSync(path.dirname(script), { recursive: true }); fs.writeFileSync(script, ""); fs.chmodSync(script, 0o755); return { command: "./scripts/run.sh --flag", cwd, envPath: undefined as NodeJS.ProcessEnv | undefined, expectedPath: script, expectedExecutableName: undefined, }; }, }, { name: "quoted executable", setup: () => { const dir = makeTempDir(); const cwd = path.join(dir, "project"); const script = path.join(cwd, "bin", "tool"); fs.mkdirSync(path.dirname(script), { recursive: true }); fs.writeFileSync(script, ""); fs.chmodSync(script, 0o755); return { command: '"./bin/tool" --version', cwd, envPath: undefined as NodeJS.ProcessEnv | undefined, expectedPath: script, expectedExecutableName: undefined, }; }, }, ] as const; for (const testCase of cases) { const setup = testCase.setup(); const res = resolveCommandResolution(setup.command, setup.cwd, setup.envPath); expect(res?.resolvedPath, testCase.name).toBe(setup.expectedPath); if (setup.expectedExecutableName) { expect(res?.executableName, testCase.name).toBe(setup.expectedExecutableName); } } }); it("unwraps transparent env wrapper argv to resolve the effective executable", () => { const fixture = createPathExecutableFixture(); const resolution = resolveCommandResolutionFromArgv( ["/usr/bin/env", "rg", "-n", "needle"], undefined, makePathEnv(fixture.binDir), ); expect(resolution?.resolvedPath).toBe(fixture.exePath); expect(resolution?.executableName).toBe(fixture.exeName); }); it("blocks semantic env wrappers from allowlist/safeBins auto-resolution", () => { const resolution = resolveCommandResolutionFromArgv([ "/usr/bin/env", "FOO=bar", "rg", "-n", "needle", ]); expect(resolution?.policyBlocked).toBe(true); expect(resolution?.rawExecutable).toBe("/usr/bin/env"); }); it("fails closed for env -S even when env itself is allowlisted", () => { const dir = makeTempDir(); const binDir = path.join(dir, "bin"); fs.mkdirSync(binDir, { recursive: true }); const envName = process.platform === "win32" ? "env.exe" : "env"; const envPath = path.join(binDir, envName); fs.writeFileSync(envPath, process.platform === "win32" ? "" : "#!/bin/sh\n"); if (process.platform !== "win32") { fs.chmodSync(envPath, 0o755); } const { analysis, allowlistEval } = analyzeEnvWrapperAllowlist({ argv: [envPath, "-S", 'sh -c "echo pwned"'], envPath: envPath, cwd: dir, }); expect(analysis.ok).toBe(true); expect(analysis.segments[0]?.resolution?.policyBlocked).toBe(true); expect(allowlistEval.allowlistSatisfied).toBe(false); expect(allowlistEval.segmentSatisfiedBy).toEqual([null]); }); it("fails closed when transparent env wrappers exceed unwrap depth", () => { if (process.platform === "win32") { return; } const dir = makeTempDir(); const binDir = path.join(dir, "bin"); fs.mkdirSync(binDir, { recursive: true }); const envPath = path.join(binDir, "env"); fs.writeFileSync(envPath, "#!/bin/sh\n"); fs.chmodSync(envPath, 0o755); const { analysis, allowlistEval } = analyzeEnvWrapperAllowlist({ argv: buildNestedEnvShellCommand({ envExecutable: envPath, depth: 5, payload: "echo pwned", }), envPath, cwd: dir, }); expect(analysis.ok).toBe(true); expect(analysis.segments[0]?.resolution?.policyBlocked).toBe(true); expect(analysis.segments[0]?.resolution?.blockedWrapper).toBe("env"); expect(allowlistEval.allowlistSatisfied).toBe(false); expect(allowlistEval.segmentSatisfiedBy).toEqual([null]); }); it("unwraps env wrapper with shell inner executable", () => { const resolution = resolveCommandResolutionFromArgv(["/usr/bin/env", "bash", "-lc", "echo hi"]); expect(resolution?.rawExecutable).toBe("bash"); expect(resolution?.executableName.toLowerCase()).toContain("bash"); }); it("unwraps nice wrapper argv to resolve the effective executable", () => { const resolution = resolveCommandResolutionFromArgv([ "/usr/bin/nice", "bash", "-lc", "echo hi", ]); expect(resolution?.rawExecutable).toBe("bash"); expect(resolution?.executableName.toLowerCase()).toContain("bash"); }); }); describe("exec approvals shell parsing", () => { it("parses pipelines and chained commands", () => { const cases = [ { name: "pipeline", command: "echo ok | jq .foo", expectedSegments: ["echo", "jq"], }, { name: "chain", command: "ls && rm -rf /", expectedChainHeads: ["ls", "rm"], }, ] as const; for (const testCase of cases) { const res = analyzeShellCommand({ command: testCase.command }); expect(res.ok, testCase.name).toBe(true); if ("expectedSegments" in testCase) { expect( res.segments.map((seg) => seg.argv[0]), testCase.name, ).toEqual(testCase.expectedSegments); } else { expect( res.chains?.map((chain) => chain[0]?.argv[0]), testCase.name, ).toEqual(testCase.expectedChainHeads); } } }); it("parses argv commands", () => { const res = analyzeArgvCommand({ argv: ["/bin/echo", "ok"] }); expect(res.ok).toBe(true); expect(res.segments[0]?.argv).toEqual(["/bin/echo", "ok"]); }); it("rejects unsupported shell constructs", () => { const cases: Array<{ command: string; reason: string; platform?: NodeJS.Platform }> = [ { command: 'echo "output: $(whoami)"', reason: "unsupported shell token: $()" }, { command: 'echo "output: `id`"', reason: "unsupported shell token: `" }, { command: "echo $(whoami)", reason: "unsupported shell token: $()" }, { command: "cat < input.txt", reason: "unsupported shell token: <" }, { command: "echo ok > output.txt", reason: "unsupported shell token: >" }, { command: "/usr/bin/echo first line\n/usr/bin/echo second line", reason: "unsupported shell token: \n", }, { command: 'echo "ok $\\\n(id -u)"', reason: "unsupported shell token: newline", }, { command: 'echo "ok $\\\r\n(id -u)"', reason: "unsupported shell token: newline", }, { command: "ping 127.0.0.1 -n 1 & whoami", reason: "unsupported windows shell token: &", platform: "win32", }, ]; for (const testCase of cases) { const res = analyzeShellCommand({ command: testCase.command, platform: testCase.platform }); expect(res.ok).toBe(false); expect(res.reason).toBe(testCase.reason); } }); it("accepts inert substitution-like syntax", () => { const cases = ['echo "output: \\$(whoami)"', "echo 'output: $(whoami)'"]; for (const command of cases) { const res = analyzeShellCommand({ command }); expect(res.ok).toBe(true); expect(res.segments[0]?.argv[0]).toBe("echo"); } }); it("accepts safe heredoc forms", () => { const cases: Array<{ command: string; expectedArgv: string[] }> = [ { command: "/usr/bin/tee /tmp/file << 'EOF'\nEOF", expectedArgv: ["/usr/bin/tee"] }, { command: "/usr/bin/tee /tmp/file < segment.argv[0])).toEqual(testCase.expectedArgv); } }); it("rejects unsafe or malformed heredoc forms", () => { const cases: Array<{ command: string; reason: string }> = [ { command: "/usr/bin/cat < { const res = analyzeShellCommand({ command: '"C:\\Program Files\\Tool\\tool.exe" --version', platform: "win32", }); expect(res.ok).toBe(true); expect(res.segments[0]?.argv).toEqual(["C:\\Program Files\\Tool\\tool.exe", "--version"]); }); it("normalizes short option clusters with attached payloads", () => { const parsed = parseExecArgvToken("-oblocked.txt"); expect(parsed.kind).toBe("option"); if (parsed.kind !== "option" || parsed.style !== "short-cluster") { throw new Error("expected short-cluster option"); } expect(parsed.flags[0]).toBe("-o"); expect(parsed.cluster).toBe("oblocked.txt"); }); it("normalizes long options with inline payloads", () => { const parsed = parseExecArgvToken("--output=blocked.txt"); expect(parsed.kind).toBe("option"); if (parsed.kind !== "option" || parsed.style !== "long") { throw new Error("expected long option"); } expect(parsed.flag).toBe("--output"); expect(parsed.inlineValue).toBe("blocked.txt"); }); }); describe("exec approvals shell allowlist (chained commands)", () => { it("evaluates chained command allowlist scenarios", () => { const cases: Array<{ allowlist: ExecAllowlistEntry[]; command: string; expectedAnalysisOk: boolean; expectedAllowlistSatisfied: boolean; platform?: NodeJS.Platform; }> = [ { allowlist: [{ pattern: "/usr/bin/obsidian-cli" }, { pattern: "/usr/bin/head" }], command: "/usr/bin/obsidian-cli print-default && /usr/bin/obsidian-cli search foo | /usr/bin/head", expectedAnalysisOk: true, expectedAllowlistSatisfied: true, }, { allowlist: [{ pattern: "/usr/bin/obsidian-cli" }], command: "/usr/bin/obsidian-cli print-default && /usr/bin/rm -rf /", expectedAnalysisOk: true, expectedAllowlistSatisfied: false, }, { allowlist: [{ pattern: "/usr/bin/echo" }], command: "/usr/bin/echo ok &&", expectedAnalysisOk: false, expectedAllowlistSatisfied: false, }, { allowlist: [{ pattern: "/usr/bin/ping" }], command: "ping 127.0.0.1 -n 1 & whoami", expectedAnalysisOk: false, expectedAllowlistSatisfied: false, platform: "win32", }, ]; for (const testCase of cases) { const result = evaluateShellAllowlist({ command: testCase.command, allowlist: testCase.allowlist, safeBins: new Set(), cwd: "/tmp", platform: testCase.platform, }); expect(result.analysisOk).toBe(testCase.expectedAnalysisOk); expect(result.allowlistSatisfied).toBe(testCase.expectedAllowlistSatisfied); } }); it("respects quoted chain separators", () => { const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/echo" }]; const commands = ['/usr/bin/echo "foo && bar"', '/usr/bin/echo "foo\\" && bar"']; for (const command of commands) { const result = evaluateShellAllowlist({ command, allowlist, safeBins: new Set(), cwd: "/tmp", }); expect(result.analysisOk).toBe(true); expect(result.allowlistSatisfied).toBe(true); } }); it("fails allowlist analysis for shell line continuations", () => { const result = evaluateShellAllowlist({ command: 'echo "ok $\\\n(id -u)"', allowlist: [{ pattern: "/usr/bin/echo" }], safeBins: new Set(), cwd: "/tmp", }); expect(result.analysisOk).toBe(false); expect(result.allowlistSatisfied).toBe(false); }); it("satisfies allowlist when bare * wildcard is present", () => { const dir = makeTempDir(); const binPath = path.join(dir, "mybin"); fs.writeFileSync(binPath, "#!/bin/sh\n", { mode: 0o755 }); const env = makePathEnv(dir); try { const result = evaluateShellAllowlist({ command: "mybin --flag", allowlist: [{ pattern: "*" }], safeBins: new Set(), cwd: dir, env, }); expect(result.analysisOk).toBe(true); expect(result.allowlistSatisfied).toBe(true); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); }); describe("exec approvals allowlist evaluation", () => { function evaluateAutoAllowSkills(params: { analysis: { ok: boolean; segments: Array<{ raw: string; argv: string[]; resolution: { rawExecutable: string; executableName: string; resolvedPath?: string; }; }>; }; 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: { 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: { 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: { 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: { 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: { 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: { 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: { rawExecutable: "tool", resolvedPath: "/usr/bin/tool", executableName: "tool", }, }; const safeBinSegment = { raw: "jq .foo", argv: ["jq", ".foo"], resolution: { 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"]); }); }); describe("exec approvals policy helpers", () => { it("minSecurity returns the more restrictive value", () => { expect(minSecurity("deny", "full")).toBe("deny"); expect(minSecurity("allowlist", "full")).toBe("allowlist"); }); it("maxAsk returns the more aggressive ask mode", () => { expect(maxAsk("off", "always")).toBe("always"); expect(maxAsk("on-miss", "off")).toBe("on-miss"); }); it("requiresExecApproval respects ask mode and allowlist satisfaction", () => { expect( requiresExecApproval({ ask: "always", security: "allowlist", analysisOk: true, allowlistSatisfied: true, }), ).toBe(true); expect( requiresExecApproval({ ask: "off", security: "allowlist", analysisOk: true, allowlistSatisfied: false, }), ).toBe(false); expect( requiresExecApproval({ ask: "on-miss", security: "allowlist", analysisOk: true, allowlistSatisfied: true, }), ).toBe(false); expect( requiresExecApproval({ ask: "on-miss", security: "allowlist", analysisOk: false, allowlistSatisfied: false, }), ).toBe(true); expect( requiresExecApproval({ ask: "on-miss", security: "full", analysisOk: false, allowlistSatisfied: false, }), ).toBe(false); }); });