diff --git a/src/node-host/invoke-system-run-plan.test.ts b/src/node-host/invoke-system-run-plan.test.ts index 33423e66681..ace75953415 100644 --- a/src/node-host/invoke-system-run-plan.test.ts +++ b/src/node-host/invoke-system-run-plan.test.ts @@ -112,14 +112,6 @@ function writeFakeRuntimeBin(binDir: string, binName: string) { } } -function withFakeRuntimeBin(params: { binName: string; run: () => T }): T { - return withFakeRuntimeBins({ - binNames: [params.binName], - tmpPrefix: `openclaw-${params.binName}-bin-`, - run: params.run, - }); -} - function withFakeRuntimeBins(params: { binNames: string[]; tmpPrefix?: string; @@ -146,6 +138,20 @@ function withFakeRuntimeBins(params: { } } +function uniqueRuntimeBinNames( + cases: ReadonlyArray>, +): string[] { + return [ + ...new Set( + cases.flatMap( + (runtimeCase) => + runtimeCase.binNames ?? + (runtimeCase.binName ? [runtimeCase.binName] : ["bunx", "pnpm", "npm", "npx", "tsx"]), + ), + ), + ]; +} + function resolveNativeBinaryFixturePath(): string { for (const candidate of ["/bin/ls", "/usr/bin/ls", "/bin/echo", "/usr/bin/printf"]) { try { @@ -790,17 +796,14 @@ describe("hardenApprovedExecutionPaths", () => { it("captures mutable runtime operands in approval plans", () => { const tmp = createFixtureDir("openclaw-approval-script-plan-"); - for (const runtimeCase of mutableOperandCases) { - runNamedCase(runtimeCase.name, () => { - if (runtimeCase.skipOnWin32 && process.platform === "win32") { - return; - } - const binNames = - runtimeCase.binNames ?? - (runtimeCase.binName ? [runtimeCase.binName] : ["bunx", "pnpm", "npm", "npx", "tsx"]); - withFakeRuntimeBins({ - binNames, - run: () => { + withFakeRuntimeBins({ + binNames: uniqueRuntimeBinNames(mutableOperandCases), + run: () => { + for (const runtimeCase of mutableOperandCases) { + runNamedCase(runtimeCase.name, () => { + if (runtimeCase.skipOnWin32 && process.platform === "win32") { + return; + } const fixture = createScriptOperandFixture(tmp, runtimeCase); writeScriptOperandFixture(fixture); const executablePath = fixture.command[0]; @@ -810,10 +813,10 @@ describe("hardenApprovedExecutionPaths", () => { fs.chmodSync(shimPath, 0o755); } expectMutableFileOperandApprovalPlan(fixture, tmp); - }, - }); - }); - } + }); + } + }, + }); }); it("captures mutable shell script operands in approval plans", () => { @@ -987,18 +990,18 @@ describe("hardenApprovedExecutionPaths", () => { }); it("rejects unsafe runtime invocation forms", () => { - for (const testCase of unsafeRuntimeInvocationCases) { - runNamedCase(testCase.name, () => { - withFakeRuntimeBin({ - binName: testCase.binName, - run: () => { + withFakeRuntimeBins({ + binNames: [...new Set(unsafeRuntimeInvocationCases.map((testCase) => testCase.binName))], + run: () => { + for (const testCase of unsafeRuntimeInvocationCases) { + runNamedCase(testCase.name, () => { const tmp = createFixtureDir(testCase.tmpPrefix); testCase.setup?.(tmp); expectRuntimeApprovalDenied(testCase.command, tmp); - }, - }); - }); - } + }); + } + }, + }); }); it("detects rewritten script operands for pnpm dlx approval plans", () => { diff --git a/src/node-host/invoke-system-run.test.ts b/src/node-host/invoke-system-run.test.ts index 290ee225e38..0656b4a399c 100644 --- a/src/node-host/invoke-system-run.test.ts +++ b/src/node-host/invoke-system-run.test.ts @@ -935,7 +935,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { expectInvokeOk(sendInvokeResult); }); - it("validates approved runtime script operand stability at dispatch", async () => { + it("validates approved runtime script operand bindings at dispatch", async () => { await withFakeRuntimeOnPath({ runtime: "tsx", run: async () => { @@ -993,41 +993,37 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { expect(stableRun.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(stableRun.sendInvokeResult); - }, - }); - }); - it("denies approval-based execution when tsx is missing a required mutable script binding", async () => { - await withFakeRuntimeOnPath({ - runtime: "tsx", - run: async () => { - const tmp = createFixtureDir("openclaw-approval-tsx-missing-binding-"); - const fixture = createRuntimeScriptOperandFixture({ tmp, runtime: "tsx" }); - fs.writeFileSync(fixture.scriptPath, fixture.initialBody); - const prepared = buildSystemRunApprovalPlan({ - command: fixture.command, - cwd: tmp, + const missingBindingTmp = createFixtureDir("openclaw-approval-tsx-missing-binding-"); + const missingBindingFixture = createRuntimeScriptOperandFixture({ + tmp: missingBindingTmp, + runtime: "tsx", }); - expect(prepared.ok).toBe(true); - if (!prepared.ok) { + fs.writeFileSync(missingBindingFixture.scriptPath, missingBindingFixture.initialBody); + const missingBindingPrepared = buildSystemRunApprovalPlan({ + command: missingBindingFixture.command, + cwd: missingBindingTmp, + }); + expect(missingBindingPrepared.ok).toBe(true); + if (!missingBindingPrepared.ok) { throw new Error("unreachable"); } - const planWithoutBinding = { ...prepared.plan }; + const planWithoutBinding = { ...missingBindingPrepared.plan }; delete planWithoutBinding.mutableFileOperand; - const { runCommand, sendInvokeResult } = await runSystemInvoke({ + const missingBindingRun = await runSystemInvoke({ preferMacAppExecHost: false, - command: prepared.plan.argv, - rawCommand: prepared.plan.commandText, + command: missingBindingPrepared.plan.argv, + rawCommand: missingBindingPrepared.plan.commandText, systemRunPlan: planWithoutBinding, - cwd: prepared.plan.cwd ?? tmp, + cwd: missingBindingPrepared.plan.cwd ?? missingBindingTmp, approved: true, security: "full", ask: "off", }); - expect(runCommand).not.toHaveBeenCalled(); - expectInvokeErrorMessage(sendInvokeResult, { + expect(missingBindingRun.runCommand).not.toHaveBeenCalled(); + expectInvokeErrorMessage(missingBindingRun.sendInvokeResult, { message: "SYSTEM_RUN_DENIED: approval missing script operand binding", exact: true, });