


























@@ -420,6 +420,37 @@ describe("stageBundledPluginRuntimeDeps", () => {
420420expect(fs.existsSync(path.join(targetPath, "owner.json"))).toBe(false);
421421});
422422423+it("retries transient backup cleanup during atomic replace", () => {
424+const parentDir = createTempDir("openclaw-runtime-deps-replace-");
425+const targetPath = path.join(parentDir, "node_modules");
426+const sourcePath = path.join(parentDir, "source-node_modules");
427+fs.mkdirSync(targetPath, { recursive: true });
428+fs.writeFileSync(path.join(targetPath, "marker.txt"), "original\n", "utf8");
429+fs.mkdirSync(sourcePath, { recursive: true });
430+fs.writeFileSync(path.join(sourcePath, "marker.txt"), "replacement\n", "utf8");
431+432+const realRmSync = fs.rmSync.bind(fs);
433+let transientFailures = 0;
434+vi.spyOn(fs, "rmSync").mockImplementation((target, options) => {
435+const targetString = String(target);
436+if (
437+targetString.includes(`${path.sep}.openclaw-runtime-deps-backup-`) &&
438+transientFailures < 2
439+) {
440+transientFailures += 1;
441+const error = new Error("transient backup cleanup failure") as NodeJS.ErrnoException;
442+error.code = "ENOTEMPTY";
443+throw error;
444+}
445+return realRmSync(target, options);
446+});
447+448+stageBundledPluginRuntimeDepsTesting.replaceDirAtomically(targetPath, sourcePath);
449+450+expect(transientFailures).toBe(2);
451+expect(fs.readFileSync(path.join(targetPath, "marker.txt"), "utf8")).toBe("replacement\n");
452+});
453+423454it("restages when installed root runtime dependency contents change", () => {
424455const { pluginDir, repoRoot } = createBundledPluginFixture({
425456packageJson: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。