From 760501fc385d99d9e6796bbcb9227db31e4e2cb9 Mon Sep 17 00:00:00 2001 From: kinjitakabe <273844887+kinjitakabe@users.noreply.github.com> Date: Tue, 12 May 2026 13:04:19 +0900 Subject: [PATCH] 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 `/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 `/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. --- extensions/matrix/src/matrix/deps.test.ts | 46 ++++++++++++++++- extensions/matrix/src/matrix/deps.ts | 61 ++++++----------------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/extensions/matrix/src/matrix/deps.test.ts b/extensions/matrix/src/matrix/deps.test.ts index 6e3c10e1f6f..f950de54a15 100644 --- a/extensions/matrix/src/matrix/deps.test.ts +++ b/extensions/matrix/src/matrix/deps.test.ts @@ -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(); + }); +}); diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index cb0f42c49c6..665f2f5e352 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -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; + resolveFn?: (id: string) => string; }): Promise { - 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)); }