fix(matrix): stop runtime npm install from parent-derived cwd

`ensureMatrixSdkInstalled` previously derived an install `cwd` via fixed
two-segment traversal from `import.meta.url` and spawned `npm install`
(or `pnpm install`) when Matrix packages were missing. Under the
externalized plugin layout the derived path is a scope directory like
`<config>/npm/node_modules/@openclaw`, so npm walks up to the managed
project root and prunes undeclared siblings. Under the legacy bundled
layout it would target `<global-prefix>/lib/node_modules` and could
delete unrelated global CLIs.

Matrix is now a pure availability check: if any required package fails
to resolve, it throws an actionable error pointing the operator at the
supported repair commands (`openclaw plugins update matrix`,
`openclaw doctor --fix`). This matches extensions/AGENTS.md:
"Runtime never installs deps; install/update/doctor are repair points."

The exported signature stays backwards-compatible (all params optional;
`confirm` and `runtime` are accepted but ignored). `resolveMissingMatrixPackages`
gains an optional `resolveFn` seam for testability, mirroring the existing
`ensureMatrixCryptoRuntime` injection pattern.

Fixes #80758.
This commit is contained in:
kinjitakabe
2026-05-12 13:04:19 +09:00
committed by Ayaan Zaidi
parent 067e83d121
commit 760501fc38
2 changed files with 60 additions and 47 deletions

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { ensureMatrixCryptoRuntime } from "./deps.js";
import { ensureMatrixCryptoRuntime, ensureMatrixSdkInstalled } from "./deps.js";
const logStub = vi.fn();
@@ -160,3 +160,47 @@ describe("ensureMatrixCryptoRuntime", () => {
);
});
});
describe("ensureMatrixSdkInstalled", () => {
it("returns without error when all required packages resolve", async () => {
const resolveFn = vi.fn((_id: string) => "/fake/path");
await expect(ensureMatrixSdkInstalled({ resolveFn })).resolves.toBeUndefined();
expect(resolveFn).toHaveBeenCalled();
});
it("throws actionable repair error listing every missing package", async () => {
const resolveFn = vi.fn((_id: string) => {
throw new Error("Cannot find module");
});
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(
/matrix-js-sdk.*@matrix-org\/matrix-sdk-crypto-nodejs.*@matrix-org\/matrix-sdk-crypto-wasm/s,
);
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(
/openclaw plugins update matrix/,
);
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(/openclaw doctor --fix/);
});
it("lists only the packages that fail to resolve", async () => {
const resolveFn = vi.fn((id: string) => {
if (id === "@matrix-org/matrix-sdk-crypto-wasm") {
throw new Error("Cannot find module");
}
return "/fake/path";
});
await expect(ensureMatrixSdkInstalled({ resolveFn })).rejects.toThrow(
/Matrix plugin dependencies are missing: @matrix-org\/matrix-sdk-crypto-wasm\./,
);
});
it("does not invoke the install confirm prompt when packages are missing (regression: #80758)", async () => {
const confirm = vi.fn(async () => true);
const resolveFn = vi.fn((_id: string) => {
throw new Error("Cannot find module");
});
await expect(ensureMatrixSdkInstalled({ resolveFn, confirm })).rejects.toThrow(
/Matrix plugin dependencies are missing/,
);
expect(confirm).not.toHaveBeenCalled();
});
});

View File

@@ -2,7 +2,6 @@ import { spawn } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
@@ -26,12 +25,12 @@ type MatrixCryptoRuntimeDeps = {
log?: (message: string) => void;
};
function resolveMissingMatrixPackages(): string[] {
function resolveMissingMatrixPackages(resolveFn?: (id: string) => string): string[] {
try {
const req = createRequire(import.meta.url);
const resolve = resolveFn ?? defaultResolveFn;
return REQUIRED_MATRIX_PACKAGES.filter((pkg) => {
try {
req.resolve(pkg);
resolve(pkg);
return false;
} catch {
return true;
@@ -46,9 +45,12 @@ export function isMatrixSdkAvailable(): boolean {
return resolveMissingMatrixPackages().length === 0;
}
function resolvePluginRoot(): string {
const currentDir = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(currentDir, "..", "..");
function buildMatrixDepsMissingMessage(missing: string[]): string {
const packages = missing.length > 0 ? missing.join(", ") : REQUIRED_MATRIX_PACKAGES.join(", ");
return [
`Matrix plugin dependencies are missing: ${packages}.`,
"Repair this plugin with `openclaw plugins update matrix` or run `openclaw doctor --fix`.",
].join(" ");
}
type CommandResult = {
@@ -299,47 +301,14 @@ async function ensureMatrixCryptoRuntimeOnce(params: MatrixCryptoRuntimeDeps): P
requireFn("@matrix-org/matrix-sdk-crypto-nodejs");
}
export async function ensureMatrixSdkInstalled(params: {
runtime: RuntimeEnv;
export async function ensureMatrixSdkInstalled(params?: {
runtime?: RuntimeEnv;
confirm?: (message: string) => Promise<boolean>;
resolveFn?: (id: string) => string;
}): Promise<void> {
if (isMatrixSdkAvailable()) {
const missing = resolveMissingMatrixPackages(params?.resolveFn);
if (missing.length === 0) {
return;
}
const confirm = params.confirm;
if (confirm) {
const ok = await confirm(
"Matrix requires matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, and @matrix-org/matrix-sdk-crypto-wasm. Install now?",
);
if (!ok) {
throw new Error(
"Matrix requires matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, and @matrix-org/matrix-sdk-crypto-wasm (install dependencies first).",
);
}
}
const root = resolvePluginRoot();
const command = fs.existsSync(path.join(root, "pnpm-lock.yaml"))
? ["pnpm", "install"]
: ["npm", "install", "--omit=dev", "--silent"];
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})…`);
const result = await runFixedCommandWithTimeout({
argv: command,
cwd: root,
timeoutMs: 300_000,
env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
});
if (result.code !== 0) {
throw new Error(
result.stderr.trim() || result.stdout.trim() || "Matrix dependency install failed.",
);
}
if (!isMatrixSdkAvailable()) {
const missing = resolveMissingMatrixPackages();
throw new Error(
missing.length > 0
? `Matrix dependency install completed but required packages are still missing: ${missing.join(", ")}`
: "Matrix dependency install completed but Matrix dependencies are still missing.",
);
}
throw new Error(buildMatrixDepsMissingMessage(missing));
}