fix(runtime): make dist-runtime staging idempotent

This commit is contained in:
Vincent Koc 2026-03-22 22:36:05 -07:00
parent 5822892fee
commit fd5555d5be
2 changed files with 62 additions and 4 deletions

View File

@ -12,8 +12,30 @@ function relativeSymlinkTarget(sourcePath, targetPath) {
return relativeTarget || ".";
}
function ensureSymlink(targetValue, targetPath, type) {
try {
fs.symlinkSync(targetValue, targetPath, type);
return;
} catch (error) {
if (error?.code !== "EEXIST") {
throw error;
}
}
try {
if (fs.lstatSync(targetPath).isSymbolicLink() && fs.readlinkSync(targetPath) === targetValue) {
return;
}
} catch {
// Fall through and recreate the target when inspection fails.
}
removePathIfExists(targetPath);
fs.symlinkSync(targetValue, targetPath, type);
}
function symlinkPath(sourcePath, targetPath, type) {
fs.symlinkSync(relativeSymlinkTarget(sourcePath, targetPath), targetPath, type);
ensureSymlink(relativeSymlinkTarget(sourcePath, targetPath), targetPath, type);
}
function shouldWrapRuntimeJsFile(sourcePath) {
@ -63,7 +85,7 @@ function stagePluginRuntimeOverlay(sourceDir, targetDir) {
}
if (dirent.isSymbolicLink()) {
fs.symlinkSync(fs.readlinkSync(sourcePath), targetPath);
ensureSymlink(fs.readlinkSync(sourcePath), targetPath);
continue;
}
@ -91,7 +113,7 @@ function linkPluginNodeModules(params) {
if (!fs.existsSync(params.sourcePluginNodeModulesDir)) {
return;
}
fs.symlinkSync(params.sourcePluginNodeModulesDir, runtimeNodeModulesDir, symlinkType());
ensureSymlink(params.sourcePluginNodeModulesDir, runtimeNodeModulesDir, symlinkType());
}
export function stageBundledPluginRuntime(params = {}) {

View File

@ -2,7 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs";
import { discoverOpenClawPlugins } from "./discovery.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
@ -329,4 +329,40 @@ describe("stageBundledPluginRuntime", () => {
expect(fs.existsSync(path.join(repoRoot, "dist-runtime"))).toBe(false);
});
it("tolerates EEXIST when an identical runtime symlink is materialized concurrently", () => {
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-eexist-");
const distPluginDir = path.join(repoRoot, "dist", "extensions", "feishu");
const distSkillDir = path.join(distPluginDir, "skills", "feishu-doc");
fs.mkdirSync(distSkillDir, { recursive: true });
fs.writeFileSync(path.join(distPluginDir, "index.js"), "export default {}\n", "utf8");
fs.writeFileSync(path.join(distSkillDir, "SKILL.md"), "# Feishu Doc\n", "utf8");
const realSymlinkSync = fs.symlinkSync.bind(fs);
const symlinkSpy = vi.spyOn(fs, "symlinkSync").mockImplementation(((target, link, type) => {
const linkPath = String(link);
if (linkPath.endsWith(path.join("skills", "feishu-doc", "SKILL.md"))) {
const err = Object.assign(new Error("file already exists"), { code: "EEXIST" });
realSymlinkSync(String(target), linkPath, type);
throw err;
}
return realSymlinkSync(String(target), linkPath, type);
}) as typeof fs.symlinkSync);
expect(() => stageBundledPluginRuntime({ repoRoot })).not.toThrow();
const runtimeSkillPath = path.join(
repoRoot,
"dist-runtime",
"extensions",
"feishu",
"skills",
"feishu-doc",
"SKILL.md",
);
expect(fs.lstatSync(runtimeSkillPath).isSymbolicLink()).toBe(true);
expect(fs.readFileSync(runtimeSkillPath, "utf8")).toBe("# Feishu Doc\n");
symlinkSpy.mockRestore();
});
});