mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
fix(plugins): verify bundled runtime deps installs
Verify bundled runtime dependency installs before reporting success, so a clean npm exit cannot hide packages missing from the managed runtime-deps root. Also updates the bundle command test mock for the current plugin enable-state API. Local proof: - `pnpm test src/plugins/bundle-commands.test.ts` - `pnpm test src/plugins/bundled-runtime-deps.test.ts src/commands/doctor-bundled-plugin-runtime-deps.test.ts src/plugins/loader.test.ts` - `pnpm check:changed` Co-authored-by: Colin <colin@solvely.net>
This commit is contained in:
@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/Bonjour: stop ciao mDNS watchdog failures from looping forever when the advertiser stays stuck in `probing` or `announcing`; Bonjour now disables itself for the current Gateway process after repeated failed restarts while the Gateway keeps running. Fixes #69011. Thanks @siddharthaagarwalofficial-ux, @FiredMosquito831, and @spikefcz.
|
||||
- Gateway/Fly.io: seed Control UI allowed origins from the actual runtime bind and port so CLI-driven non-loopback starts do not crash before config exists. Fixes #71823.
|
||||
- Gateway/proxy: bootstrap env proxy dispatching from direct Gateway startup so provider and plugin network requests honor `HTTPS_PROXY`/`HTTP_PROXY` before the first embedded agent attempt runs. (#71833) Thanks @mjamiv.
|
||||
- Plugins/runtime deps: verify clean npm installs actually place requested bundled runtime packages in the managed install root, reporting exact missing specs instead of a false successful repair. (#71883) Thanks @Solvely-Colin.
|
||||
- Models/LM Studio: preserve `@iq*` quant suffixes in model refs and provider matching so `/model lmstudio/...@iq3_xxs` keeps the exact LM Studio variant. Fixes #71474. (#71486) Thanks @Bartok9, @XinwuC, and @Sanjays2402.
|
||||
- Matrix/cron: preserve the live Matrix delivery target when creating implicit announce reminder jobs so mixed-case room IDs are not reconstructed from lowercased session keys. Fixes #71798.
|
||||
- Feishu: accept Schema 2.0 card action callbacks that report `context.open_chat_id` instead of legacy `context.chat_id`, so button callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.
|
||||
|
||||
@@ -34,6 +34,12 @@ vi.mock("./config-state.js", async (importOriginal) => ({
|
||||
}) => ({
|
||||
activated: params.config?.entries?.[params.id]?.enabled !== false,
|
||||
}),
|
||||
resolveEffectiveEnableState: (params: {
|
||||
config?: { entries?: Record<string, { enabled?: boolean }> };
|
||||
id: string;
|
||||
}) => ({
|
||||
enabled: params.config?.entries?.[params.id]?.enabled !== false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const { loadEnabledClaudeBundleCommands } = await import("./bundle-commands.js");
|
||||
|
||||
@@ -31,6 +31,16 @@ function makeTempDir(): string {
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeInstalledPackage(rootDir: string, packageName: string, version: string): void {
|
||||
const packageDir = path.join(rootDir, "node_modules", ...packageName.split("/"));
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify({ name: packageName, version }),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
spawnSyncMock.mockReset();
|
||||
@@ -182,13 +192,16 @@ describe("installBundledRuntimeDeps", () => {
|
||||
vi.spyOn(fs, "existsSync").mockImplementation(
|
||||
(candidate) => candidate === "C:\\node\\node_modules\\npm\\bin\\npm-cli.js",
|
||||
);
|
||||
spawnSyncMock.mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
spawnSyncMock.mockImplementation((_command, _args, options) => {
|
||||
writeInstalledPackage(String(options?.cwd ?? ""), "acpx", "0.5.3");
|
||||
return {
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
};
|
||||
});
|
||||
|
||||
installBundledRuntimeDeps({
|
||||
@@ -235,6 +248,7 @@ describe("installBundledRuntimeDeps", () => {
|
||||
name: "openclaw-runtime-deps-install",
|
||||
private: true,
|
||||
});
|
||||
writeInstalledPackage(cwd, "@grammyjs/runner", "2.0.3");
|
||||
return {
|
||||
pid: 123,
|
||||
output: [],
|
||||
@@ -317,13 +331,16 @@ describe("installBundledRuntimeDeps", () => {
|
||||
|
||||
it("uses an OpenClaw-owned npm cache for runtime dependency installs", () => {
|
||||
const installRoot = makeTempDir();
|
||||
spawnSyncMock.mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
spawnSyncMock.mockImplementation((_command, _args, options) => {
|
||||
writeInstalledPackage(String(options?.cwd ?? ""), "tokenjuice", "0.6.1");
|
||||
return {
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
};
|
||||
});
|
||||
|
||||
installBundledRuntimeDeps({
|
||||
@@ -374,6 +391,26 @@ describe("installBundledRuntimeDeps", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("fails when npm exits cleanly without installing requested packages", () => {
|
||||
const installRoot = makeTempDir();
|
||||
spawnSyncMock.mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
installBundledRuntimeDeps({
|
||||
installRoot,
|
||||
missingSpecs: ["tokenjuice@0.6.1"],
|
||||
env: {},
|
||||
}),
|
||||
).toThrow(`npm install did not place bundled runtime deps in ${installRoot}: tokenjuice@0.6.1`);
|
||||
});
|
||||
|
||||
it("cleans an owned isolated execution root after copying node_modules back", () => {
|
||||
const installRoot = makeTempDir();
|
||||
const installExecutionRoot = path.join(installRoot, ".openclaw-install-stage");
|
||||
|
||||
@@ -684,6 +684,19 @@ function hasDependencySentinel(
|
||||
});
|
||||
}
|
||||
|
||||
function assertBundledRuntimeDepsInstalled(rootDir: string, specs: readonly string[]): void {
|
||||
const missingSpecs = specs.filter((spec) => {
|
||||
const dep = parseInstallableRuntimeDepSpec(spec);
|
||||
return !hasDependencySentinel([rootDir], dep);
|
||||
});
|
||||
if (missingSpecs.length === 0) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`npm install did not place bundled runtime deps in ${rootDir}: ${missingSpecs.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
function replaceNodeModulesDir(targetDir: string, sourceDir: string): void {
|
||||
const parentDir = path.dirname(targetDir);
|
||||
const tempDir = fs.mkdtempSync(path.join(parentDir, ".openclaw-runtime-deps-copy-"));
|
||||
@@ -1223,6 +1236,7 @@ export function installBundledRuntimeDeps(params: {
|
||||
.trim();
|
||||
throw new Error(output || "npm install failed");
|
||||
}
|
||||
assertBundledRuntimeDepsInstalled(installExecutionRoot, params.missingSpecs);
|
||||
if (isolatedExecutionRoot) {
|
||||
const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
|
||||
if (!fs.existsSync(stagedNodeModulesDir)) {
|
||||
@@ -1234,6 +1248,7 @@ export function installBundledRuntimeDeps(params: {
|
||||
} else {
|
||||
replaceNodeModulesDir(targetNodeModulesDir, stagedNodeModulesDir);
|
||||
}
|
||||
assertBundledRuntimeDepsInstalled(params.installRoot, params.missingSpecs);
|
||||
}
|
||||
} finally {
|
||||
if (cleanInstallExecutionRoot) {
|
||||
|
||||
Reference in New Issue
Block a user