diff --git a/src/infra/package-update-steps.test.ts b/src/infra/package-update-steps.test.ts index 576ef703ca9..b282129112e 100644 --- a/src/infra/package-update-steps.test.ts +++ b/src/infra/package-update-steps.test.ts @@ -477,36 +477,53 @@ describe("runGlobalPackageUpdateSteps", () => { await fs.mkdir(path.dirname(targetShim), { recursive: true }); await fs.writeFile(targetShim, "old shim\n", "utf8"); - const result = await runGlobalPackageUpdateSteps({ - installTarget: createNpmTarget(globalRoot), - installSpec: "openclaw@2.0.0", - packageName: "openclaw", - packageRoot, - runCommand: createRootRunner(globalRoot), - runStep: async ({ name, argv, cwd }) => { - const prefixIndex = argv.indexOf("--prefix"); - const stagePrefix = argv[prefixIndex + 1]; - if (!stagePrefix) { - throw new Error("missing staged prefix"); + let stagedShimForFailure: string | undefined; + const realCopyFile = fs.copyFile.bind(fs); + const copyFileSpy = vi + .spyOn(fs, "copyFile") + .mockImplementation(async (...args: Parameters) => { + const [source] = args; + if (stagedShimForFailure && String(source) === stagedShimForFailure) { + throw createFsError("EACCES", "staged shim copy failed"); } - await writePackageRoot( - path.join(stagePrefix, "lib", "node_modules", "openclaw"), - "2.0.0", - ); - const stagedShim = path.join(stagePrefix, "bin", "openclaw"); - await fs.mkdir(path.dirname(stagedShim), { recursive: true }); - await fs.writeFile(stagedShim, "new shim\n", "utf8"); - await fs.chmod(stagedShim, 0); - return { - name, - command: argv.join(" "), - cwd: cwd ?? process.cwd(), - durationMs: 1, - exitCode: 0, - }; - }, - timeoutMs: 1000, - }); + return await realCopyFile(...args); + }); + + let result: Awaited>; + try { + result = await runGlobalPackageUpdateSteps({ + installTarget: createNpmTarget(globalRoot), + installSpec: "openclaw@2.0.0", + packageName: "openclaw", + packageRoot, + runCommand: createRootRunner(globalRoot), + runStep: async ({ name, argv, cwd }) => { + const prefixIndex = argv.indexOf("--prefix"); + const stagePrefix = argv[prefixIndex + 1]; + if (!stagePrefix) { + throw new Error("missing staged prefix"); + } + await writePackageRoot( + path.join(stagePrefix, "lib", "node_modules", "openclaw"), + "2.0.0", + ); + const stagedShim = path.join(stagePrefix, "bin", "openclaw"); + stagedShimForFailure = stagedShim; + await fs.mkdir(path.dirname(stagedShim), { recursive: true }); + await fs.writeFile(stagedShim, "new shim\n", "utf8"); + return { + name, + command: argv.join(" "), + cwd: cwd ?? process.cwd(), + durationMs: 1, + exitCode: 0, + }; + }, + timeoutMs: 1000, + }); + } finally { + copyFileSpy.mockRestore(); + } expect(result.failedStep?.name).toBe("global install swap"); expect(result.verifiedPackageRoot).toBe(packageRoot); diff --git a/src/node-host/invoke-system-run-plan.test.ts b/src/node-host/invoke-system-run-plan.test.ts index d8dc594248c..cd692181039 100644 --- a/src/node-host/invoke-system-run-plan.test.ts +++ b/src/node-host/invoke-system-run-plan.test.ts @@ -65,6 +65,22 @@ function sha256FileSync(filePath: string): string { return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); } +function canWritePathSync(targetPath: string): boolean { + try { + fs.accessSync(targetPath, fs.constants.W_OK); + return true; + } catch { + return false; + } +} + +function canMutateNativeBinaryFixturePath(binaryPath: string): boolean { + const realPath = fs.realpathSync(binaryPath); + return [binaryPath, path.dirname(binaryPath), realPath, path.dirname(realPath)].some((entry) => + canWritePathSync(entry), + ); +} + function createScriptOperandFixture(tmp: string, fixture?: RuntimeFixture): ScriptOperandFixture { if (fixture) { return { @@ -646,7 +662,7 @@ describe("hardenApprovedExecutionPaths", () => { ); }); - it("allows shell payloads that invoke absolute-path native binaries", () => { + it("handles shell payloads that invoke absolute-path native binaries", () => { if (process.platform === "win32") { return; } @@ -656,6 +672,10 @@ describe("hardenApprovedExecutionPaths", () => { rawCommand: binaryPath, cwd: process.cwd(), }); + if (canMutateNativeBinaryFixturePath(binaryPath)) { + expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL); + return; + } expect(prepared.ok).toBe(true); if (!prepared.ok) { throw new Error("unreachable");