fix: scope claude doctor runtime checks

This commit is contained in:
Peter Steinberger
2026-04-29 05:44:36 +01:00
parent b3a8c7146b
commit 9023b120a1
5 changed files with 312 additions and 97 deletions

View File

@@ -451,6 +451,98 @@ describe("stageBundledPluginRuntimeDeps", () => {
expect(fs.readFileSync(path.join(targetPath, "marker.txt"), "utf8")).toBe("replacement\n");
});
it("keeps a successful replacement when backup cleanup hits transient ENOTEMPTY", () => {
const parentDir = createTempDir("openclaw-runtime-deps-replace-cleanup-");
const targetPath = path.join(parentDir, "node_modules");
const sourcePath = path.join(parentDir, "source-node_modules");
fs.mkdirSync(targetPath, { recursive: true });
fs.writeFileSync(path.join(targetPath, "marker.txt"), "original\n", "utf8");
fs.mkdirSync(sourcePath, { recursive: true });
fs.writeFileSync(path.join(sourcePath, "marker.txt"), "replacement\n", "utf8");
const realRenameSync = fs.renameSync.bind(fs);
const realRmSync = fs.rmSync.bind(fs);
let backupPath: string | null = null;
vi.spyOn(fs, "renameSync").mockImplementation((oldPath, newPath) => {
const oldPathString = String(oldPath);
const newPathString = String(newPath);
if (
oldPathString === targetPath &&
path.basename(newPathString).startsWith(".openclaw-runtime-deps-backup-")
) {
backupPath = newPathString;
}
return realRenameSync(oldPath, newPath);
});
vi.spyOn(fs, "rmSync").mockImplementation((target, options) => {
const targetString = String(target);
if (
backupPath &&
targetString === backupPath &&
fs.existsSync(path.join(backupPath, "marker.txt"))
) {
const error = new Error("Directory not empty") as NodeJS.ErrnoException;
error.code = "ENOTEMPTY";
throw error;
}
return realRmSync(target, options);
});
expect(() =>
stageBundledPluginRuntimeDepsTesting.replaceDirAtomically(targetPath, sourcePath),
).not.toThrow();
expect(fs.readFileSync(path.join(targetPath, "marker.txt"), "utf8")).toBe("replacement\n");
expect(backupPath).not.toBeNull();
expect(fs.readFileSync(path.join(backupPath ?? "", "marker.txt"), "utf8")).toBe("original\n");
expect(fs.existsSync(path.join(backupPath ?? "", "owner.json"))).toBe(true);
});
it("keeps successful root staging when owned stage temp cleanup races", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {
name: "@openclaw/fixture-plugin",
version: "1.0.0",
dependencies: { direct: "1.0.0" },
openclaw: { bundle: { stageRuntimeDependencies: true } },
},
});
const directDir = path.join(repoRoot, "node_modules", "direct");
fs.mkdirSync(directDir, { recursive: true });
fs.writeFileSync(
path.join(directDir, "package.json"),
'{ "name": "direct", "version": "1.0.0" }\n',
"utf8",
);
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'direct';\n", "utf8");
const realRmSync = fs.rmSync.bind(fs);
let cleanupAttempts = 0;
vi.spyOn(fs, "rmSync").mockImplementation((target, options) => {
const targetString = String(target);
if (
targetString.startsWith(path.join(pluginDir, ".openclaw-runtime-deps-stage-")) &&
cleanupAttempts === 0
) {
cleanupAttempts += 1;
const error = new Error("Directory not empty") as NodeJS.ErrnoException;
error.code = "ENOTEMPTY";
throw error;
}
if (targetString.startsWith(path.join(pluginDir, ".openclaw-runtime-deps-stage-"))) {
cleanupAttempts += 1;
}
return realRmSync(target, options);
});
expect(() => stageBundledPluginRuntimeDeps({ cwd: repoRoot })).not.toThrow();
expect(cleanupAttempts).toBe(2);
expect(
fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"),
).toBe("module.exports = 'direct';\n");
});
it("restages when installed root runtime dependency contents change", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {