fix(build): harden bundled plugin runtime staging

Copy bundled plugin skill trees into dist-runtime, broaden Windows symlink-copy fallbacks, and harden runtime-deps fingerprinting.
This commit is contained in:
Vincent Koc
2026-04-25 04:27:17 -07:00
committed by GitHub
parent f408bba9de
commit 443b837bd5
6 changed files with 169 additions and 35 deletions

View File

@@ -332,6 +332,64 @@ describe("stageBundledPluginRuntimeDeps", () => {
).toBe("module.exports = 'second';\n");
});
it("fingerprints regular files when readdir reports symlink-like entries", () => {
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 realReaddirSync = fs.readdirSync.bind(fs);
vi.spyOn(fs, "readdirSync").mockImplementation(((target, options) => {
const result = realReaddirSync(target, options as never);
if (
String(target) !== directDir ||
typeof options !== "object" ||
options === null ||
!("withFileTypes" in options) ||
options.withFileTypes !== true
) {
return result;
}
return (result as fs.Dirent[]).map((entry) => {
if (entry.name !== "package.json") {
return entry;
}
return {
...entry,
isSymbolicLink: () => true,
isDirectory: () => false,
isFile: () => false,
} as fs.Dirent;
}) as never;
}) as typeof fs.readdirSync);
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: () => {
installCount += 1;
throw new Error("unexpected fallback install");
},
});
expect(installCount).toBe(0);
expect(
fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"),
).toBe("module.exports = 'direct';\n");
});
it("refuses to replace a symlinked plugin node_modules directory", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {

View File

@@ -20,17 +20,9 @@ describe("stageBundledPluginRuntime", () => {
it("copies files when Windows rejects runtime overlay symlinks", async () => {
await withTempDir(async (repoRoot) => {
const sourceFile = path.join(
repoRoot,
"dist",
"extensions",
"acpx",
"skills",
"acp-router",
"fixture.txt",
);
const sourceFile = path.join(repoRoot, "dist", "extensions", "acpx", "assets", "fixture.txt");
await fs.promises.mkdir(path.dirname(sourceFile), { recursive: true });
await fs.promises.writeFile(sourceFile, "skill-body\n", "utf8");
await fs.promises.writeFile(sourceFile, "asset-body\n", "utf8");
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const symlinkSpy = vi
@@ -54,11 +46,10 @@ describe("stageBundledPluginRuntime", () => {
"dist-runtime",
"extensions",
"acpx",
"skills",
"acp-router",
"assets",
"fixture.txt",
);
expect(await fs.promises.readFile(runtimeFile, "utf8")).toBe("skill-body\n");
expect(await fs.promises.readFile(runtimeFile, "utf8")).toBe("asset-body\n");
expect(fs.lstatSync(runtimeFile).isSymbolicLink()).toBe(false);
expect(symlinkSpy).toHaveBeenCalled();
});