Skip to content

Commit a504cd0

Browse files
committed
test: make root permission assertions deterministic
1 parent f3361dc commit a504cd0

2 files changed

Lines changed: 67 additions & 30 deletions

File tree

src/infra/package-update-steps.test.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -477,36 +477,53 @@ describe("runGlobalPackageUpdateSteps", () => {
477477
await fs.mkdir(path.dirname(targetShim), { recursive: true });
478478
await fs.writeFile(targetShim, "old shim\n", "utf8");
479479

480-
const result = await runGlobalPackageUpdateSteps({
481-
installTarget: createNpmTarget(globalRoot),
482-
installSpec: "[email protected]",
483-
packageName: "openclaw",
484-
packageRoot,
485-
runCommand: createRootRunner(globalRoot),
486-
runStep: async ({ name, argv, cwd }) => {
487-
const prefixIndex = argv.indexOf("--prefix");
488-
const stagePrefix = argv[prefixIndex + 1];
489-
if (!stagePrefix) {
490-
throw new Error("missing staged prefix");
480+
let stagedShimForFailure: string | undefined;
481+
const realCopyFile = fs.copyFile.bind(fs);
482+
const copyFileSpy = vi
483+
.spyOn(fs, "copyFile")
484+
.mockImplementation(async (...args: Parameters<typeof fs.copyFile>) => {
485+
const [source] = args;
486+
if (stagedShimForFailure && String(source) === stagedShimForFailure) {
487+
throw createFsError("EACCES", "staged shim copy failed");
491488
}
492-
await writePackageRoot(
493-
path.join(stagePrefix, "lib", "node_modules", "openclaw"),
494-
"2.0.0",
495-
);
496-
const stagedShim = path.join(stagePrefix, "bin", "openclaw");
497-
await fs.mkdir(path.dirname(stagedShim), { recursive: true });
498-
await fs.writeFile(stagedShim, "new shim\n", "utf8");
499-
await fs.chmod(stagedShim, 0);
500-
return {
501-
name,
502-
command: argv.join(" "),
503-
cwd: cwd ?? process.cwd(),
504-
durationMs: 1,
505-
exitCode: 0,
506-
};
507-
},
508-
timeoutMs: 1000,
509-
});
489+
return await realCopyFile(...args);
490+
});
491+
492+
let result: Awaited<ReturnType<typeof runGlobalPackageUpdateSteps>>;
493+
try {
494+
result = await runGlobalPackageUpdateSteps({
495+
installTarget: createNpmTarget(globalRoot),
496+
installSpec: "[email protected]",
497+
packageName: "openclaw",
498+
packageRoot,
499+
runCommand: createRootRunner(globalRoot),
500+
runStep: async ({ name, argv, cwd }) => {
501+
const prefixIndex = argv.indexOf("--prefix");
502+
const stagePrefix = argv[prefixIndex + 1];
503+
if (!stagePrefix) {
504+
throw new Error("missing staged prefix");
505+
}
506+
await writePackageRoot(
507+
path.join(stagePrefix, "lib", "node_modules", "openclaw"),
508+
"2.0.0",
509+
);
510+
const stagedShim = path.join(stagePrefix, "bin", "openclaw");
511+
stagedShimForFailure = stagedShim;
512+
await fs.mkdir(path.dirname(stagedShim), { recursive: true });
513+
await fs.writeFile(stagedShim, "new shim\n", "utf8");
514+
return {
515+
name,
516+
command: argv.join(" "),
517+
cwd: cwd ?? process.cwd(),
518+
durationMs: 1,
519+
exitCode: 0,
520+
};
521+
},
522+
timeoutMs: 1000,
523+
});
524+
} finally {
525+
copyFileSpy.mockRestore();
526+
}
510527

511528
expect(result.failedStep?.name).toBe("global install swap");
512529
expect(result.verifiedPackageRoot).toBe(packageRoot);

src/node-host/invoke-system-run-plan.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ function sha256FileSync(filePath: string): string {
6565
return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
6666
}
6767

68+
function canWritePathSync(targetPath: string): boolean {
69+
try {
70+
fs.accessSync(targetPath, fs.constants.W_OK);
71+
return true;
72+
} catch {
73+
return false;
74+
}
75+
}
76+
77+
function canMutateNativeBinaryFixturePath(binaryPath: string): boolean {
78+
const realPath = fs.realpathSync(binaryPath);
79+
return [binaryPath, path.dirname(binaryPath), realPath, path.dirname(realPath)].some((entry) =>
80+
canWritePathSync(entry),
81+
);
82+
}
83+
6884
function createScriptOperandFixture(tmp: string, fixture?: RuntimeFixture): ScriptOperandFixture {
6985
if (fixture) {
7086
return {
@@ -646,7 +662,7 @@ describe("hardenApprovedExecutionPaths", () => {
646662
);
647663
});
648664

649-
it("allows shell payloads that invoke absolute-path native binaries", () => {
665+
it("handles shell payloads that invoke absolute-path native binaries", () => {
650666
if (process.platform === "win32") {
651667
return;
652668
}
@@ -656,6 +672,10 @@ describe("hardenApprovedExecutionPaths", () => {
656672
rawCommand: binaryPath,
657673
cwd: process.cwd(),
658674
});
675+
if (canMutateNativeBinaryFixturePath(binaryPath)) {
676+
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
677+
return;
678+
}
659679
expect(prepared.ok).toBe(true);
660680
if (!prepared.ok) {
661681
throw new Error("unreachable");

0 commit comments

Comments
 (0)