Files
openclaw/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs
2026-05-26 01:17:12 +02:00

224 lines
8.6 KiB
JavaScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const normalizePathForProbe = (value) => String(value ?? "").replace(/\\/g, "/");
const bundledRuntimeFragments = (pluginDir) => [
`/dist/extensions/${pluginDir}`,
`/dist-runtime/extensions/${pluginDir}`,
];
const bundledRuntimeRootFragments = ["/dist/extensions/", "/dist-runtime/extensions/"];
function resolveStateDir() {
if (process.env.OPENCLAW_STATE_DIR) {
return process.env.OPENCLAW_STATE_DIR;
}
return path.join(process.env.HOME || os.homedir(), ".openclaw");
}
function pathReferencesBundledRuntime(value, pluginDir) {
const normalized = normalizePathForProbe(value);
return bundledRuntimeFragments(pluginDir).some((fragment) => normalized.includes(fragment));
}
function pathReferencesPackagedBundledRoot(value) {
const normalized = normalizePathForProbe(value);
return bundledRuntimeRootFragments.some((fragment) => normalized.includes(fragment));
}
function resolveOpenClawEntry() {
if (process.env.OPENCLAW_ENTRY) {
return process.env.OPENCLAW_ENTRY;
}
for (const entry of ["dist/index.mjs", "dist/index.js"]) {
if (fs.existsSync(entry)) {
return entry;
}
}
throw new Error("Missing OPENCLAW_ENTRY and dist/index.(m)js");
}
function readPluginsList() {
const entry = resolveOpenClawEntry();
const result = spawnSync(process.execPath, [entry, "plugins", "list", "--json"], {
cwd: process.cwd(),
encoding: "utf8",
env: process.env,
});
if (result.status !== 0) {
throw new Error(
`Unable to list packaged bundled plugins: ${result.stderr || result.stdout || `exit ${result.status}`}`,
);
}
const payload = JSON.parse(result.stdout);
return Array.isArray(payload.plugins) ? payload.plugins : [];
}
function pluginRequiresConfig(pluginDir) {
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
if (!fs.existsSync(manifestPath)) {
throw new Error(`missing bundled plugin manifest: ${manifestPath}`);
}
const manifest = readJson(manifestPath);
const required = manifest.configSchema?.required;
return Array.isArray(required) && required.some((value) => typeof value === "string");
}
async function loadPackagedBundledEntries() {
return readPluginsList()
.filter((plugin) => plugin?.origin === "bundled")
.map((plugin) => {
const id = typeof plugin.id === "string" ? plugin.id.trim() : "";
const rootDir = typeof plugin.rootDir === "string" ? plugin.rootDir.trim() : "";
const source = typeof plugin.source === "string" ? plugin.source.trim() : "";
const pluginDir = rootDir || (source ? path.dirname(source) : "");
if (!id || !pluginDir || !pathReferencesPackagedBundledRoot(pluginDir)) {
return null;
}
return {
id,
dir: path.basename(pluginDir),
rootDir: pluginDir,
requiresConfig: pluginRequiresConfig(pluginDir),
};
})
.filter(Boolean)
.toSorted((a, b) => a.id.localeCompare(b.id));
}
async function loadManifestEntries() {
const explicit = (process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "")
.split(/[,\s]+/u)
.map((entry) => entry.trim())
.filter(Boolean);
const manifestEntries = await loadPackagedBundledEntries();
if (explicit.length === 0) {
return manifestEntries;
}
const available = manifestEntries.map((entry) => entry.id).join(", ");
return explicit.map((lookup) => {
const found = manifestEntries.find((entry) => entry.id === lookup || entry.dir === lookup);
if (!found) {
throw new Error(
`OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS entry is not an installable bundled plugin in this package: ${lookup}. Available: ${available}`,
);
}
return found;
});
}
async function selectedManifestEntries() {
const allEntries = await loadManifestEntries();
const total = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL || "1", 10);
const index = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX || "0", 10);
if (!Number.isInteger(total) || total < 1) {
throw new Error(
`OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL must be >= 1, got ${process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL}`,
);
}
if (!Number.isInteger(index) || index < 0 || index >= total) {
throw new Error(
`OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX must be in [0, ${total - 1}], got ${process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX}`,
);
}
const selected = allEntries.filter((_, candidateIndex) => candidateIndex % total === index);
if (selected.length === 0) {
throw new Error(`No bundled plugin ids selected for shard ${index}/${total}`);
}
return selected;
}
function assertInstalled(pluginId, pluginDir, requiresConfig) {
const stateDir = resolveStateDir();
const configPath = path.join(stateDir, "openclaw.json");
const indexPath = path.join(stateDir, "plugins", "installs.json");
const config = readJson(configPath);
const index = readJson(indexPath);
const records = index.installRecords ?? index.records ?? {};
const record = records[pluginId];
if (!record) {
throw new Error(`missing install record for ${pluginId}`);
}
if (record.source !== "path") {
throw new Error(
`expected bundled install record source=path for ${pluginId}, got ${record.source}`,
);
}
if (
typeof record.sourcePath !== "string" ||
!pathReferencesBundledRuntime(record.sourcePath, pluginDir)
) {
throw new Error(`unexpected bundled source path for ${pluginId}: ${record.sourcePath}`);
}
if (normalizePathForProbe(record.installPath) !== normalizePathForProbe(record.sourcePath)) {
throw new Error(`bundled install path should equal source path for ${pluginId}`);
}
const paths = config.plugins?.load?.paths || [];
if (paths.some((entry) => pathReferencesBundledRuntime(entry, pluginDir))) {
throw new Error(`config load paths should not include bundled install path for ${pluginId}`);
}
if (requiresConfig && config.plugins?.entries?.[pluginId]?.enabled === true) {
throw new Error(
`plugin requiring config should not be enabled immediately after install for ${pluginId}`,
);
}
if (!requiresConfig && config.plugins?.entries?.[pluginId]?.enabled !== true) {
throw new Error(`config entry is not enabled after install for ${pluginId}`);
}
const allow = config.plugins?.allow || [];
if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) {
throw new Error(`existing allowlist does not include ${pluginId} after install`);
}
if ((config.plugins?.deny || []).includes(pluginId)) {
throw new Error(`denylist contains ${pluginId} after install`);
}
}
function assertUninstalled(pluginId, pluginDir) {
const stateDir = resolveStateDir();
const configPath = path.join(stateDir, "openclaw.json");
const indexPath = path.join(stateDir, "plugins", "installs.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const records = index.installRecords ?? index.records ?? {};
if (records[pluginId]) {
throw new Error(`install record still present after uninstall for ${pluginId}`);
}
const paths = config.plugins?.load?.paths || [];
if (paths.some((entry) => pathReferencesBundledRuntime(entry, pluginDir))) {
throw new Error(`load path still present after uninstall for ${pluginId}`);
}
if (config.plugins?.entries?.[pluginId]) {
throw new Error(`config entry still present after uninstall for ${pluginId}`);
}
if ((config.plugins?.allow || []).includes(pluginId)) {
throw new Error(`allowlist still contains ${pluginId} after uninstall`);
}
if ((config.plugins?.deny || []).includes(pluginId)) {
throw new Error(`denylist still contains ${pluginId} after uninstall`);
}
const managedPath = path.join(stateDir, "extensions", pluginId);
if (fs.existsSync(managedPath)) {
throw new Error(
`managed install directory unexpectedly exists for bundled plugin ${pluginId}: ${managedPath}`,
);
}
}
const [command, pluginId, pluginDir, requiresConfig] = process.argv.slice(2);
if (command === "select") {
for (const entry of await selectedManifestEntries()) {
console.log(`${entry.id}\t${entry.dir}\t${entry.requiresConfig ? "1" : "0"}\t${entry.rootDir}`);
}
} else if (command === "assert-installed") {
assertInstalled(pluginId, pluginDir, requiresConfig === "1");
} else if (command === "assert-uninstalled") {
assertUninstalled(pluginId, pluginDir);
} else {
throw new Error(`Unknown bundled plugin probe command: ${command || "(missing)"}`);
}