mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 03:22:40 +00:00
* test(qa): prove marketplace plugin updates * fix(plugins): clean exact install load paths on uninstall * fix(ci): register marketplace lifecycle assertions
225 lines
8.2 KiB
JavaScript
225 lines
8.2 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { readPluginInstallIndex } from "../plugin-index-sqlite.mjs";
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
function stateDir() {
|
|
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME ?? "", ".openclaw");
|
|
}
|
|
|
|
function configPath() {
|
|
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
|
|
}
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
}
|
|
|
|
function resolveRecordedPath(value) {
|
|
if (value === "~") {
|
|
return process.env.HOME ?? "";
|
|
}
|
|
if (value.startsWith("~/")) {
|
|
return path.join(process.env.HOME ?? "", value.slice(2));
|
|
}
|
|
return path.resolve(value);
|
|
}
|
|
|
|
function resolveComparablePath(value) {
|
|
const resolved = resolveRecordedPath(value);
|
|
try {
|
|
return fs.realpathSync(resolved);
|
|
} catch {
|
|
return resolved;
|
|
}
|
|
}
|
|
|
|
function assertPathInside(parent, child, label) {
|
|
const relative = path.relative(fs.realpathSync(parent), fs.realpathSync(child));
|
|
assert(
|
|
relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative),
|
|
`${label} escaped ${parent}: ${child}`,
|
|
);
|
|
}
|
|
|
|
function samePath(left, right) {
|
|
return resolveComparablePath(left) === resolveComparablePath(right);
|
|
}
|
|
|
|
function appendUnique(values, additions) {
|
|
return [...new Set([...(values ?? []), ...additions])];
|
|
}
|
|
|
|
function assertMarketplaceState() {
|
|
const pluginId = process.argv[3];
|
|
const expectedVersion = process.argv[4];
|
|
const expectedMarketplaceSource = process.argv[5];
|
|
const expectedMarketplacePlugin = process.argv[6];
|
|
const installPathFile = process.argv[7];
|
|
assert(pluginId, "missing plugin id");
|
|
assert(expectedVersion, "missing expected version");
|
|
assert(expectedMarketplaceSource, "missing expected marketplace source");
|
|
assert(expectedMarketplacePlugin, "missing expected marketplace plugin");
|
|
assert(installPathFile, "missing install path file");
|
|
|
|
const databasePath = path.join(stateDir(), "state", "openclaw.sqlite");
|
|
assert(fs.existsSync(databasePath), `canonical plugin index database missing: ${databasePath}`);
|
|
const index = readPluginInstallIndex({ stateDir: stateDir(), configPath: configPath() });
|
|
const record = index.installRecords?.[pluginId];
|
|
assert(record, `missing install record for ${pluginId}`);
|
|
assert(record.source === "marketplace", `expected marketplace source, got ${record.source}`);
|
|
assert(
|
|
record.marketplaceSource === expectedMarketplaceSource,
|
|
`expected marketplace source ${expectedMarketplaceSource}, got ${record.marketplaceSource}`,
|
|
);
|
|
assert(
|
|
record.marketplacePlugin === expectedMarketplacePlugin,
|
|
`expected marketplace plugin ${expectedMarketplacePlugin}, got ${record.marketplacePlugin}`,
|
|
);
|
|
assert(
|
|
record.version === expectedVersion,
|
|
`expected install record version ${expectedVersion}, got ${record.version}`,
|
|
);
|
|
|
|
const indexedPlugin = (index.plugins ?? []).find((entry) => entry.pluginId === pluginId);
|
|
assert(indexedPlugin, `installed plugin index omitted ${pluginId}`);
|
|
assert(
|
|
indexedPlugin.packageVersion === expectedVersion,
|
|
`expected indexed package version ${expectedVersion}, got ${indexedPlugin.packageVersion}`,
|
|
);
|
|
|
|
const config = readJson(configPath());
|
|
assert(
|
|
config.plugins?.entries?.[pluginId]?.enabled === true,
|
|
`plugin config entry is not enabled for ${pluginId}`,
|
|
);
|
|
|
|
assert(record.installPath, `install path missing for ${pluginId}`);
|
|
const installPath = resolveRecordedPath(record.installPath);
|
|
assert(
|
|
fs.existsSync(installPath),
|
|
`managed install path missing for ${pluginId}: ${installPath}`,
|
|
);
|
|
assertPathInside(path.join(stateDir(), "extensions"), installPath, "managed install path");
|
|
const packageJson = readJson(path.join(installPath, "package.json"));
|
|
assert(
|
|
packageJson.version === expectedVersion,
|
|
`expected installed package version ${expectedVersion}, got ${packageJson.version}`,
|
|
);
|
|
|
|
if (fs.existsSync(installPathFile)) {
|
|
const previousInstallPath = fs.readFileSync(installPathFile, "utf8").trim();
|
|
assert(
|
|
previousInstallPath === installPath,
|
|
`plugin update moved the managed install path: ${previousInstallPath} -> ${installPath}`,
|
|
);
|
|
} else {
|
|
fs.writeFileSync(installPathFile, `${installPath}\n`, "utf8");
|
|
}
|
|
}
|
|
|
|
function seedMarketplaceUninstallState() {
|
|
const pluginId = process.argv[3];
|
|
const sentinelPluginId = process.argv[4];
|
|
const sentinelPath = process.argv[5];
|
|
const installPathFile = process.argv[6];
|
|
assert(pluginId, "missing plugin id");
|
|
assert(sentinelPluginId, "missing sentinel plugin id");
|
|
assert(sentinelPath, "missing sentinel path");
|
|
assert(installPathFile, "missing install path file");
|
|
|
|
const index = readPluginInstallIndex({ stateDir: stateDir(), configPath: configPath() });
|
|
const installPath = index.installRecords?.[pluginId]?.installPath;
|
|
assert(installPath, `install path missing for ${pluginId}`);
|
|
assert(
|
|
fs.existsSync(installPath),
|
|
`managed install path missing before uninstall: ${installPath}`,
|
|
);
|
|
assert(fs.existsSync(sentinelPath), `sentinel path missing before uninstall: ${sentinelPath}`);
|
|
|
|
const config = readJson(configPath());
|
|
const plugins = config.plugins ?? {};
|
|
config.plugins = {
|
|
...plugins,
|
|
allow: appendUnique(plugins.allow, [pluginId, sentinelPluginId]),
|
|
deny: appendUnique(plugins.deny, [pluginId, sentinelPluginId]),
|
|
load: {
|
|
...plugins.load,
|
|
paths: appendUnique(plugins.load?.paths, [installPath, sentinelPath]),
|
|
},
|
|
};
|
|
fs.writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
fs.writeFileSync(installPathFile, `${installPath}\n`, "utf8");
|
|
}
|
|
|
|
function assertMarketplaceUninstalled() {
|
|
const pluginId = process.argv[3];
|
|
const sentinelPluginId = process.argv[4];
|
|
const sentinelPath = process.argv[5];
|
|
const installPathFile = process.argv[6];
|
|
assert(pluginId, "missing plugin id");
|
|
assert(sentinelPluginId, "missing sentinel plugin id");
|
|
assert(sentinelPath, "missing sentinel path");
|
|
assert(installPathFile, "missing install path file");
|
|
|
|
const installPath = fs.readFileSync(installPathFile, "utf8").trim();
|
|
const index = readPluginInstallIndex({ stateDir: stateDir(), configPath: configPath() });
|
|
const config = readJson(configPath());
|
|
const loadPaths = config.plugins?.load?.paths ?? [];
|
|
|
|
assert(!index.installRecords?.[pluginId], `install record still present for ${pluginId}`);
|
|
assert(
|
|
!(index.plugins ?? []).some((entry) => entry.pluginId === pluginId),
|
|
`installed plugin index still includes ${pluginId}`,
|
|
);
|
|
assert(!config.plugins?.entries?.[pluginId], `plugin config entry still present for ${pluginId}`);
|
|
assert(!config.plugins?.allow?.includes(pluginId), `allowlist still includes ${pluginId}`);
|
|
assert(!config.plugins?.deny?.includes(pluginId), `denylist still includes ${pluginId}`);
|
|
assert(
|
|
config.plugins?.allow?.includes(sentinelPluginId),
|
|
`allowlist lost sentinel ${sentinelPluginId}`,
|
|
);
|
|
assert(
|
|
config.plugins?.deny?.includes(sentinelPluginId),
|
|
`denylist lost sentinel ${sentinelPluginId}`,
|
|
);
|
|
assert(
|
|
!loadPaths.some((candidate) => samePath(candidate, installPath)),
|
|
`load paths still include managed install path ${installPath}`,
|
|
);
|
|
assert(
|
|
loadPaths.some((candidate) => samePath(candidate, sentinelPath)),
|
|
`load paths lost sentinel path ${sentinelPath}`,
|
|
);
|
|
assert(!fs.existsSync(installPath), `managed install path still exists: ${installPath}`);
|
|
assert(fs.existsSync(sentinelPath), `sentinel path was removed: ${sentinelPath}`);
|
|
}
|
|
|
|
function assertUpdateLog() {
|
|
const logPath = process.argv[3];
|
|
const expected = process.argv[4];
|
|
assert(logPath, "missing update log path");
|
|
assert(expected, "missing expected update log text");
|
|
const output = fs.readFileSync(logPath, "utf8");
|
|
assert(output.includes(expected), `${logPath} did not contain ${expected}`);
|
|
}
|
|
|
|
const commands = {
|
|
"assert-marketplace-state": assertMarketplaceState,
|
|
"seed-marketplace-uninstall-state": seedMarketplaceUninstallState,
|
|
"assert-marketplace-uninstalled": assertMarketplaceUninstalled,
|
|
"assert-update-log": assertUpdateLog,
|
|
};
|
|
|
|
const command = process.argv[2];
|
|
const fn = commands[command];
|
|
if (!fn) {
|
|
throw new Error(`unknown marketplace lifecycle assertion command: ${command ?? "<missing>"}`);
|
|
}
|
|
fn();
|