test: broaden plugin install update coverage

This commit is contained in:
Peter Steinberger
2026-05-02 02:57:00 +01:00
parent 62b20e7fa2
commit 7ed73f5383
12 changed files with 416 additions and 11 deletions

View File

@@ -86,6 +86,39 @@ function writePluginWithCli([dir, id, version, method, name, cliRoot, cliOutput]
writePluginManifest(path.join(dir, "openclaw.plugin.json"), id);
}
function writePluginWithCliRegistryDependency([
dir,
id,
version,
method,
name,
cliRoot,
cliOutput,
]) {
for (const [value, label] of [
[dir, "dir"],
[id, "id"],
[version, "version"],
[method, "method"],
[name, "name"],
[cliRoot, "cliRoot"],
[cliOutput, "cliOutput"],
]) {
requireArg(value, label);
}
writeJson(path.join(dir, "package.json"), {
name: `@openclaw/${id}`,
version,
dependencies: { "is-number": "7.0.0" },
openclaw: { extensions: ["./index.js"] },
});
write(
path.join(dir, "index.js"),
`const isNumber = require("is-number");\nmodule.exports = { id: ${JSON.stringify(id)}, name: ${JSON.stringify(name)}, register(api) { api.registerGatewayMethod(${JSON.stringify(method)}, async () => ({ ok: isNumber(42) })); api.registerCli(({ program }) => { const root = program.command(${JSON.stringify(cliRoot)}).description(${JSON.stringify(`${name} fixture command`)}); root.command("ping").description("Print fixture ping output").action(() => { console.log(${JSON.stringify(cliOutput)}); }); }, { descriptors: [{ name: ${JSON.stringify(cliRoot)}, description: ${JSON.stringify(`${name} fixture command`)}, hasSubcommands: true }] }); }, };\n`,
);
writePluginManifest(path.join(dir, "openclaw.plugin.json"), id);
}
function writeClaudeBundle([root]) {
root = requireArg(root, "root");
writeJson(path.join(root, ".claude-plugin", "plugin.json"), { name: "claude-bundle-e2e" });
@@ -128,6 +161,8 @@ export const pluginCommands = {
plugin: writePlugin,
"plugin-vendored-dep": writePluginWithVendoredDependency,
"plugin-cli": writePluginWithCli,
"plugin-cli-registry-dep": writePluginWithCliRegistryDependency,
"fake-is-number-package": ([dir]) => writeFakeIsNumberPackage(requireArg(dir, "dir")),
"plugin-manifest": ([file, id]) =>
writePluginManifest(requireArg(file, "file"), requireArg(id, "id")),
"claude-bundle": writeClaudeBundle,

View File

@@ -101,6 +101,15 @@ function assertSimplePlugin(jsonFile, inspectFile, pluginId, method) {
}
}
function assertUpdateOutput(logFile, expectedSnippet) {
const output = fs.readFileSync(logFile, "utf8");
if (!output.includes(expectedSnippet)) {
throw new Error(
`expected update output to include ${JSON.stringify(expectedSnippet)}:\n${output}`,
);
}
}
function assertClaudeBundleDisabled() {
const data = readJson("/tmp/plugins-bundle-disabled.json");
const plugin = (data.plugins || []).find((entry) => entry.id === "claude-bundle-e2e");
@@ -326,6 +335,62 @@ function assertPluginDirDeps() {
assertRealPathInside(installPath, dependencyPackagePath, "local plugin copied dependency");
}
function assertLocalPathUpdateSkipped() {
assertUpdateOutput("/tmp/plugins-dir-update.log", 'Skipping "demo-plugin-dir" (source: path).');
}
function assertNpmPlugin() {
assertSimplePlugin(
"/tmp/plugins-npm.json",
"/tmp/plugins-npm-inspect.json",
"demo-plugin-npm",
"demo.npm",
);
const inspect = readJson("/tmp/plugins-npm-inspect.json");
if (!Array.isArray(inspect.cliCommands) || !inspect.cliCommands.includes("demo-npm")) {
throw new Error(`expected demo-npm cli command, got ${inspect.cliCommands?.join(", ")}`);
}
const cliOutput = fs.readFileSync("/tmp/plugins-npm-cli.txt", "utf8");
if (!cliOutput.includes("demo-plugin-npm:pong")) {
throw new Error(`unexpected npm plugin cli output: ${cliOutput.trim()}`);
}
const record = getInstallRecords()["demo-plugin-npm"];
if (!record) {
throw new Error("missing npm install record for demo-plugin-npm");
}
if (record.source !== "npm") {
throw new Error(`unexpected npm install source: ${record.source}`);
}
if (record.spec !== "@openclaw/demo-plugin-npm@0.0.1") {
throw new Error(`unexpected npm spec: ${record.spec}`);
}
if (record.resolvedName !== "@openclaw/demo-plugin-npm") {
throw new Error(`unexpected npm resolved name: ${record.resolvedName}`);
}
if (record.resolvedVersion !== "0.0.1") {
throw new Error(`unexpected npm resolved version: ${record.resolvedVersion}`);
}
const installPath = record.installPath?.replace(/^~(?=$|\/)/u, process.env.HOME);
if (!installPath || !fs.existsSync(installPath)) {
throw new Error(`npm install path missing on disk: ${installPath}`);
}
const nodeModulesRoot = path.dirname(path.dirname(installPath));
const npmRoot = path.dirname(nodeModulesRoot);
const dependencyPackagePath = path.join(nodeModulesRoot, "is-number", "package.json");
if (!fs.existsSync(dependencyPackagePath)) {
throw new Error(`missing npm plugin installed dependency: ${dependencyPackagePath}`);
}
assertRealPathInside(npmRoot, dependencyPackagePath, "npm plugin installed dependency");
}
function assertNpmPluginUpdateUnchanged() {
assertUpdateOutput("/tmp/plugins-npm-update.log", "demo-plugin-npm is up to date (0.0.1).");
assertNpmPlugin();
}
function assertMarketplaceUpdated() {
const data = readJson("/tmp/plugins-marketplace-updated.json");
const inspect = readJson("/tmp/plugins-marketplace-updated-inspect.json");
@@ -341,6 +406,49 @@ function assertMarketplaceUpdated() {
}
}
function assertGitPluginUpdated() {
const beforeCommit = process.argv[3];
assertSimplePlugin(
"/tmp/plugins-git-update.json",
"/tmp/plugins-git-update-inspect.json",
"demo-plugin-git-update",
"demo.git.update.v2",
);
const inspect = readJson("/tmp/plugins-git-update-inspect.json");
if (!Array.isArray(inspect.cliCommands) || !inspect.cliCommands.includes("demo-git-update")) {
throw new Error(`expected demo-git-update cli command, got ${inspect.cliCommands?.join(", ")}`);
}
const cliOutput = fs.readFileSync("/tmp/plugins-git-update-cli.txt", "utf8");
if (!cliOutput.includes("demo-plugin-git-update:pong-v2")) {
throw new Error(`unexpected updated git plugin cli output: ${cliOutput.trim()}`);
}
const record = getInstallRecords()["demo-plugin-git-update"];
if (!record) {
throw new Error("missing git update install record for demo-plugin-git-update");
}
if (record.source !== "git") {
throw new Error(`unexpected git update source: ${record.source}`);
}
if (record.gitRef !== "main") {
throw new Error(`unexpected git update ref: ${record.gitRef}`);
}
if (!record.gitCommit || record.gitCommit === beforeCommit) {
throw new Error(
`expected git update commit to advance from ${beforeCommit}, got ${record.gitCommit}`,
);
}
if (record.version !== "0.0.2") {
throw new Error(`unexpected git update version: ${record.version}`);
}
assertUpdateOutput(
"/tmp/plugins-git-update.log",
"Updated demo-plugin-git-update: 0.0.1 -> 0.0.2.",
);
}
async function assertClawHubPreflight() {
const spec = process.env.CLAWHUB_PLUGIN_SPEC;
if (!spec?.startsWith("clawhub:")) {
@@ -476,6 +584,14 @@ function assertClawHubRemoved() {
}
}
function assertClawHubUpdated() {
assertUpdateOutput(
"/tmp/plugins-clawhub-update.log",
`${process.env.CLAWHUB_PLUGIN_ID} already at 0.1.0.`,
);
assertClawHubInstalled();
}
const commands = {
"record-fixture-plugin-trust": recordFixturePluginTrust,
"demo-plugin": assertDemoPlugin,
@@ -493,6 +609,7 @@ const commands = {
"demo-plugin-dir",
"demo.dir",
),
"plugin-dir-update-skipped": assertLocalPathUpdateSkipped,
"plugin-dir-deps": assertPluginDirDeps,
"plugin-file": () =>
assertSimplePlugin(
@@ -501,16 +618,20 @@ const commands = {
"demo-plugin-file",
"demo.file",
),
"plugin-npm": assertNpmPlugin,
"plugin-npm-update": assertNpmPluginUpdateUnchanged,
"bundle-disabled": assertClaudeBundleDisabled,
"bundle-inspect": assertClaudeBundleInspect,
"slash-install": assertSlashInstall,
"plugin-git": assertGitPlugin,
"plugin-git-updated": assertGitPluginUpdated,
"marketplace-list": assertMarketplaceList,
"marketplace-installed": assertMarketplaceInstalled,
"marketplace-records": assertMarketplaceRecords,
"marketplace-updated": assertMarketplaceUpdated,
"clawhub-preflight": assertClawHubPreflight,
"clawhub-installed": assertClawHubInstalled,
"clawhub-updated": assertClawHubUpdated,
"clawhub-removed": assertClawHubRemoved,
};

View File

@@ -49,6 +49,12 @@ run_plugins_clawhub_scenario() {
node scripts/e2e/lib/plugins/assertions.mjs clawhub-installed
node "$OPENCLAW_ENTRY" plugins update "$CLAWHUB_PLUGIN_ID" >/tmp/plugins-clawhub-update.log 2>&1
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-clawhub-updated.json
node "$OPENCLAW_ENTRY" plugins inspect "$CLAWHUB_PLUGIN_ID" --json >/tmp/plugins-clawhub-updated-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs clawhub-updated
run_logged uninstall-clawhub node "$OPENCLAW_ENTRY" plugins uninstall "$CLAWHUB_PLUGIN_SPEC" --force
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-clawhub-uninstalled.json

View File

@@ -32,6 +32,30 @@ write_fixture_plugin_with_cli() {
node scripts/e2e/lib/fixture.mjs plugin-cli "$dir" "$id" "$version" "$method" "$name" "$cli_root" "$cli_output"
}
pack_fixture_plugin_with_cli_registry_dependency() {
local pack_dir="$1"
local output_tgz="$2"
local id="$3"
local version="$4"
local method="$5"
local name="$6"
local cli_root="$7"
local cli_output="$8"
mkdir -p "$pack_dir/package"
node scripts/e2e/lib/fixture.mjs plugin-cli-registry-dep "$pack_dir/package" "$id" "$version" "$method" "$name" "$cli_root" "$cli_output"
tar -czf "$output_tgz" -C "$pack_dir" package
}
pack_fake_is_number_package() {
local pack_dir="$1"
local output_tgz="$2"
mkdir -p "$pack_dir/package"
node scripts/e2e/lib/fixture.mjs fake-is-number-package "$pack_dir/package"
tar -czf "$output_tgz" -C "$pack_dir" package
}
write_fixture_plugin_with_vendored_dependency() {
local dir="$1"
local id="$2"
@@ -62,6 +86,39 @@ pack_fixture_plugin() {
tar -czf "$output_tgz" -C "$pack_dir" package
}
start_npm_fixture_registry() {
local package_name="$1"
local version="$2"
local tarball="$3"
local fixture_dir="$4"
local server_log="$fixture_dir/npm-registry.log"
local server_port_file="$fixture_dir/npm-registry-port"
local server_pid_file="$fixture_dir/npm-registry-pid"
shift 4
node scripts/e2e/lib/plugins/npm-registry-server.mjs "$server_port_file" "$package_name" "$version" "$tarball" "$@" >"$server_log" 2>&1 &
local server_pid="$!"
echo "$server_pid" >"$server_pid_file"
for _ in $(seq 1 100); do
if [[ -s "$server_port_file" ]]; then
export NPM_CONFIG_REGISTRY="http://127.0.0.1:$(cat "$server_port_file")"
trap 'if [[ -f "'"$server_pid_file"'" ]]; then kill "$(cat "'"$server_pid_file"'")" 2>/dev/null || true; fi' EXIT
return 0
fi
if ! kill -0 "$server_pid" 2>/dev/null; then
cat "$server_log"
return 1
fi
sleep 0.1
done
cat "$server_log"
echo "Timed out waiting for npm fixture registry." >&2
return 1
}
write_claude_bundle_fixture() {
local bundle_root="$1"

View File

@@ -0,0 +1,99 @@
import crypto from "node:crypto";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
const [portFile, ...packageArgs] = process.argv.slice(2);
if (!portFile || packageArgs.length === 0 || packageArgs.length % 3 !== 0) {
console.error(
"usage: npm-registry-server.mjs <port-file> <package-name> <version> <tarball-path> [...]",
);
process.exit(1);
}
const packages = new Map();
for (let index = 0; index < packageArgs.length; index += 3) {
const packageName = packageArgs[index];
const version = packageArgs[index + 1];
const tarballPath = packageArgs[index + 2];
const archive = fs.readFileSync(tarballPath);
packages.set(packageName, {
archive,
dependencies: packageName === "@openclaw/demo-plugin-npm" ? { "is-number": "7.0.0" } : {},
encodedPackageName: encodeURIComponent(packageName).replace("%40", "@"),
integrity: `sha512-${crypto.createHash("sha512").update(archive).digest("base64")}`,
packageName,
shasum: crypto.createHash("sha1").update(archive).digest("hex"),
tarballName: path.basename(tarballPath),
version,
});
}
const metadataFor = (entry, baseUrl) => ({
name: entry.packageName,
"dist-tags": { latest: entry.version },
versions: {
[entry.version]: {
dependencies: entry.dependencies,
name: entry.packageName,
version: entry.version,
dist: {
integrity: entry.integrity,
shasum: entry.shasum,
tarball: `${baseUrl}/${entry.encodedPackageName}/-/${entry.tarballName}`,
},
},
},
});
function findPackageForPath(pathname) {
return packages.get(decodeURIComponent(pathname.slice(1)));
}
function findTarballForPath(pathname) {
for (const entry of packages.values()) {
const prefix = `/${entry.encodedPackageName}/-/`;
if (
pathname.toLowerCase().startsWith(prefix.toLowerCase()) &&
pathname.endsWith(`/${entry.tarballName}`)
) {
return entry;
}
}
return undefined;
}
const server = http.createServer((request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
const baseUrl = `http://127.0.0.1:${server.address().port}`;
if (request.method !== "GET") {
response.writeHead(405, { "content-type": "text/plain" });
response.end("method not allowed");
return;
}
const packageEntry = findPackageForPath(url.pathname);
if (packageEntry) {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify(metadataFor(packageEntry, baseUrl))}\n`);
return;
}
const tarballEntry = findTarballForPath(url.pathname);
if (tarballEntry) {
response.writeHead(200, {
"content-type": "application/octet-stream",
"content-length": String(tarballEntry.archive.length),
});
response.end(tarballEntry.archive);
return;
}
response.writeHead(404, { "content-type": "text/plain" });
response.end(`not found: ${url.pathname}`);
});
server.listen(0, "127.0.0.1", () => {
fs.writeFileSync(portFile, String(server.address().port));
});

View File

@@ -46,6 +46,9 @@ node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-dir --runtime --json >/tmp/pl
node scripts/e2e/lib/plugins/assertions.mjs plugin-dir
node "$OPENCLAW_ENTRY" plugins update demo-plugin-dir >/tmp/plugins-dir-update.log 2>&1
node scripts/e2e/lib/plugins/assertions.mjs plugin-dir-update-skipped
echo "Testing install from local folder with preinstalled dependencies..."
dir_deps_plugin="$(mktemp -d "/tmp/openclaw-plugin-dir-deps.XXXXXX")"
write_fixture_plugin_with_vendored_dependency "$dir_deps_plugin" demo-plugin-dir-deps 0.0.1 demo.dir.deps "Demo Plugin DIR Deps"
@@ -66,6 +69,24 @@ node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-file --runtime --json >/tmp/p
node scripts/e2e/lib/plugins/assertions.mjs plugin-file
echo "Testing install and update from npm registry..."
npm_pack_dir="$(mktemp -d "/tmp/openclaw-plugin-npm-pack.XXXXXX")"
npm_dep_pack_dir="$(mktemp -d "/tmp/openclaw-plugin-npm-dep-pack.XXXXXX")"
npm_registry_dir="$(mktemp -d "/tmp/openclaw-plugin-npm-registry.XXXXXX")"
pack_fixture_plugin_with_cli_registry_dependency "$npm_pack_dir" /tmp/demo-plugin-npm.tgz demo-plugin-npm 0.0.1 demo.npm "Demo Plugin NPM" demo-npm "demo-plugin-npm:pong"
pack_fake_is_number_package "$npm_dep_pack_dir" /tmp/is-number-7.0.0.tgz
start_npm_fixture_registry "@openclaw/demo-plugin-npm" "0.0.1" /tmp/demo-plugin-npm.tgz "$npm_registry_dir" "is-number" "7.0.0" /tmp/is-number-7.0.0.tgz
run_logged install-npm node "$OPENCLAW_ENTRY" plugins install "npm:@openclaw/demo-plugin-npm@0.0.1"
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-npm.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-npm --runtime --json >/tmp/plugins-npm-inspect.json
run_logged exec-npm-plugin-cli bash -c 'node "$OPENCLAW_ENTRY" demo-npm ping >/tmp/plugins-npm-cli.txt'
node scripts/e2e/lib/plugins/assertions.mjs plugin-npm
node "$OPENCLAW_ENTRY" plugins update demo-plugin-npm >/tmp/plugins-npm-update.log 2>&1
node scripts/e2e/lib/plugins/assertions.mjs plugin-npm-update
echo "Testing install from git repo and plugin CLI execution..."
git_fixture_root="$(mktemp -d "/tmp/openclaw-plugin-git.XXXXXX")"
git_repo="$git_fixture_root/repo"
@@ -85,6 +106,31 @@ run_logged exec-git-plugin-cli bash -c 'node "$OPENCLAW_ENTRY" demo-git ping >/t
node scripts/e2e/lib/plugins/assertions.mjs plugin-git "$git_repo_url" "$git_ref"
echo "Testing git plugin update from moving ref..."
git_update_fixture_root="$(mktemp -d "/tmp/openclaw-plugin-git-update.XXXXXX")"
git_update_repo="$git_update_fixture_root/repo"
git_update_repo_url="file://$git_update_repo"
write_fixture_plugin_with_cli "$git_update_repo" demo-plugin-git-update 0.0.1 demo.git.update.v1 "Demo Plugin Git Update" demo-git-update "demo-plugin-git-update:pong-v1"
git -C "$git_update_repo" init -q
git -C "$git_update_repo" config user.email "docker-e2e@openclaw.local"
git -C "$git_update_repo" config user.name "OpenClaw Docker E2E"
git -C "$git_update_repo" checkout -qb main
git -C "$git_update_repo" add -A
git -C "$git_update_repo" commit -qm "test fixture v1"
git_update_ref_v1="$(git -C "$git_update_repo" rev-parse HEAD)"
run_logged install-git-update node "$OPENCLAW_ENTRY" plugins install "git:$git_update_repo_url@main"
write_fixture_plugin_with_cli "$git_update_repo" demo-plugin-git-update 0.0.2 demo.git.update.v2 "Demo Plugin Git Update" demo-git-update "demo-plugin-git-update:pong-v2"
git -C "$git_update_repo" add -A
git -C "$git_update_repo" commit -qm "test fixture v2"
node "$OPENCLAW_ENTRY" plugins update demo-plugin-git-update >/tmp/plugins-git-update.log 2>&1
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-git-update.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-git-update --runtime --json >/tmp/plugins-git-update-inspect.json
run_logged exec-updated-git-plugin-cli bash -c 'node "$OPENCLAW_ENTRY" demo-git-update ping >/tmp/plugins-git-update-cli.txt'
node scripts/e2e/lib/plugins/assertions.mjs plugin-git-updated "$git_update_ref_v1"
echo "Testing Claude bundle enable and inspect flow..."
bundle_plugin_id="claude-bundle-e2e"
bundle_root="$OPENCLAW_PLUGIN_HOME/$bundle_plugin_id"