fix: harden ACP plugin tools bridge (#56867) (thanks @joe2643)

This commit is contained in:
Peter Steinberger
2026-03-29 21:03:43 +01:00
parent e24091413c
commit 73477eee4c
14 changed files with 612 additions and 47 deletions

View File

@@ -0,0 +1,95 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createNestedNpmInstallEnv,
runBundledPluginPostinstall,
} from "../../scripts/postinstall-bundled-plugins.mjs";
const cleanupDirs: string[] = [];
afterEach(async () => {
await Promise.all(
cleanupDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
});
async function createExtensionsDir() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-postinstall-"));
cleanupDirs.push(root);
const extensionsDir = path.join(root, "dist", "extensions");
await fs.mkdir(path.join(extensionsDir, "acpx"), { recursive: true });
await fs.writeFile(path.join(extensionsDir, "acpx", "package.json"), "{}\n", "utf8");
return extensionsDir;
}
describe("bundled plugin postinstall", () => {
it("clears global npm config before nested installs", () => {
expect(
createNestedNpmInstallEnv({
npm_config_global: "true",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
}),
).toEqual({
HOME: "/tmp/home",
});
});
it("installs bundled plugin deps only during global installs", async () => {
const extensionsDir = await createExtensionsDir();
const execSync = vi.fn();
runBundledPluginPostinstall({
env: { npm_config_global: "false" },
extensionsDir,
execSync,
});
expect(execSync).not.toHaveBeenCalled();
});
it("runs nested local installs with sanitized env when the sentinel package is missing", async () => {
const extensionsDir = await createExtensionsDir();
const execSync = vi.fn();
runBundledPluginPostinstall({
env: {
npm_config_global: "true",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
execSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expect(execSync).toHaveBeenCalledWith("npm install --omit=dev --no-save --package-lock=false", {
cwd: path.join(extensionsDir, "acpx"),
env: {
HOME: "/tmp/home",
},
stdio: "pipe",
});
});
it("skips reinstall when the bundled sentinel package already exists", async () => {
const extensionsDir = await createExtensionsDir();
await fs.mkdir(path.join(extensionsDir, "acpx", "node_modules", "acpx"), { recursive: true });
await fs.writeFile(
path.join(extensionsDir, "acpx", "node_modules", "acpx", "package.json"),
"{}\n",
"utf8",
);
const execSync = vi.fn();
runBundledPluginPostinstall({
env: { npm_config_global: "true" },
extensionsDir,
execSync,
});
expect(execSync).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,53 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { copyStaticExtensionAssets } from "../../scripts/runtime-postbuild.mjs";
const cleanupDirs: string[] = [];
afterEach(async () => {
await Promise.all(
cleanupDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
});
async function createTempRoot() {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-runtime-postbuild-"));
cleanupDirs.push(dir);
return dir;
}
describe("runtime postbuild static assets", () => {
it("copies declared static assets into dist", async () => {
const rootDir = await createTempRoot();
const src = "extensions/acpx/src/runtime-internals/mcp-proxy.mjs";
const dest = "dist/extensions/acpx/mcp-proxy.mjs";
const sourcePath = path.join(rootDir, src);
const destPath = path.join(rootDir, dest);
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(sourcePath, "proxy-data\n", "utf8");
copyStaticExtensionAssets({
rootDir,
assets: [{ src, dest }],
});
expect(await fs.readFile(destPath, "utf8")).toBe("proxy-data\n");
});
it("warns when a declared static asset is missing", async () => {
const rootDir = await createTempRoot();
const warn = vi.fn();
copyStaticExtensionAssets({
rootDir,
assets: [{ src: "missing/file.mjs", dest: "dist/file.mjs" }],
warn,
});
expect(warn).toHaveBeenCalledWith(
"[runtime-postbuild] static asset not found, skipping: missing/file.mjs",
);
});
});