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

@@ -33,8 +33,8 @@ Update and plugin tests protect these contracts:
- Plugin npm dependencies are installed in the managed npm root, scanned before
trust, and removed through npm during uninstall so hoisted dependencies do not
linger.
- Plugin update is stable when nothing changed: install records, resolved source,
and enabled state stay intact.
- Plugin update is stable when nothing changed: install records, resolved
source, installed dependency layout, and enabled state stay intact.
## Local proof during development
@@ -83,9 +83,11 @@ pnpm test:docker:update-migration
Important lanes:
- `test:docker:plugins` validates plugin install smoke, local folder installs,
local folders with preinstalled dependencies, git installs with package
dependencies, npm package dependency installs, local ClawHub fixture installs,
marketplace update behavior, and Claude-bundle enable/inspect. Set
local folder update skip behavior, local folders with preinstalled
dependencies, `file:` package installs, git installs with CLI execution, git
moving-ref updates, npm registry installs with hoisted transitive
dependencies, npm update no-ops, local ClawHub fixture installs and update
no-ops, marketplace update behavior, and Claude-bundle enable/inspect. Set
`OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to keep the ClawHub block hermetic/offline.
- `test:docker:plugin-update` validates that an unchanged installed plugin does
not reinstall or lose install metadata during `openclaw plugins update`.
@@ -234,6 +236,10 @@ can fail for the right reason:
- Published-release migration behavior: `published-upgrade-survivor` scenario.
- Registry/package source behavior: `test:docker:plugins` fixture or ClawHub
fixture server.
- Dependency layout or cleanup behavior: assert both runtime execution and the
filesystem boundary. npm dependencies may be hoisted under the managed npm
root, so tests should prove the root is scanned/cleaned instead of assuming a
package-local `node_modules` tree.
Keep new Docker fixtures hermetic by default. Use local fixture registries and
fake packages unless the point of the test is live registry behavior.

View File

@@ -632,11 +632,11 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
- MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`)
- Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`)
- Cron/subagent MCP cleanup (real Gateway + stdio MCP child teardown after isolated cron and one-shot subagent runs): `pnpm test:docker:cron-mcp-cleanup` (script: `scripts/e2e/cron-mcp-cleanup-docker.sh`)
- Plugins (install smoke, ClawHub kitchen-sink install/uninstall, marketplace updates, and Claude-bundle enable/inspect): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
- Plugins (install/update smoke for local path, `file:`, npm registry with hoisted dependencies, git moving refs, ClawHub kitchen-sink, marketplace updates, and Claude-bundle enable/inspect): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the ClawHub block, or override the default kitchen-sink package/runtime pair with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`. Without `OPENCLAW_CLAWHUB_URL`/`CLAWHUB_URL`, the test uses a hermetic local ClawHub fixture server.
- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`)
- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`)
- Plugins: `pnpm test:docker:plugins` covers install smoke, local ClawHub fixture installs, marketplace updates, npm package dependency installs, and Claude-bundle enable/inspect. `pnpm test:docker:plugin-update` covers unchanged update behavior for installed plugins.
- Plugins: `pnpm test:docker:plugins` covers install/update smoke for local path, `file:`, npm registry with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect. `pnpm test:docker:plugin-update` covers unchanged update behavior for installed plugins.
To prebuild and reuse the shared functional image manually:

View File

@@ -46,6 +46,11 @@ npm installs run in the npm root with:
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --ignore-scripts --no-audit --no-fund
```
npm may hoist transitive dependencies to `~/.openclaw/npm/node_modules` beside
the plugin package. OpenClaw scans the managed npm root before trusting the
install and uses npm to remove npm-managed packages during uninstall, so hoisted
runtime dependencies stay inside the managed cleanup boundary.
git installs clone or refresh the repository, then run:
```bash
@@ -53,7 +58,8 @@ npm install --omit=dev --ignore-scripts --no-audit --no-fund
```
The installed plugin then loads from that package directory, so package-local
`node_modules` resolution works the same way it does for a normal Node package.
and parent `node_modules` resolution works the same way it does for a normal
Node package.
## Local plugins

View File

@@ -46,6 +46,7 @@ title: "Tests"
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
- `pnpm test:docker:update-migration`: Runs the published-upgrade survivor harness in the cleanup-heavy `plugin-deps-cleanup` scenario, starting at `openclaw@2026.4.23` by default. The separate `Update Migration` workflow expands this lane with `baselines=all-since-2026.4.23` so every stable published package from `.23` onward updates to the candidate and proves configured-plugin dependency cleanup outside Full Release CI.
- `pnpm test:docker:plugins`: Runs install/update smoke for local path, `file:`, npm registry packages with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect.
## Local PR gate

View File

@@ -100,9 +100,10 @@ Plugin dependency installation happens only during explicit install/update or
doctor repair flows. Gateway startup, config reload, and runtime inspection do
not run package managers or repair dependency trees. Local plugins must already
have their dependencies installed, while npm, git, and ClawHub plugins are
installed under OpenClaw's managed plugin roots with package-local
dependencies. External plugins and custom load paths must still be installed
through `openclaw plugins install`.
installed under OpenClaw's managed plugin roots. npm dependencies may be hoisted
within OpenClaw's managed npm root; install/update scans that managed root before
trust and uninstall removes npm-managed packages through npm. External plugins
and custom load paths must still be installed through `openclaw plugins install`.
See [Plugin dependency resolution](/plugins/dependency-resolution) for the
install-time lifecycle.

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"

View File

@@ -24,6 +24,7 @@ const PLUGINS_DOCKER_SWEEP_PATH = "scripts/e2e/lib/plugins/sweep.sh";
const PLUGINS_DOCKER_MARKETPLACE_PATH = "scripts/e2e/lib/plugins/marketplace.sh";
const PLUGINS_DOCKER_CLAWHUB_PATH = "scripts/e2e/lib/plugins/clawhub.sh";
const PLUGINS_DOCKER_ASSERTIONS_PATH = "scripts/e2e/lib/plugins/assertions.mjs";
const PLUGINS_DOCKER_NPM_REGISTRY_PATH = "scripts/e2e/lib/plugins/npm-registry-server.mjs";
const PLUGIN_UPDATE_DOCKER_E2E_PATH = "scripts/e2e/plugin-update-unchanged-docker.sh";
const PLUGIN_UPDATE_SCENARIO_PATH = "scripts/e2e/lib/plugin-update/unchanged-scenario.sh";
const PLUGIN_UPDATE_PROBE_PATH = "scripts/e2e/lib/plugin-update/probe.mjs";
@@ -274,4 +275,30 @@ describe("docker build helper", () => {
expect(clawhub).toContain("live ClawHub can rate-limit CI");
expect(clawhub).toContain('[[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]');
});
it("covers plugin install/update sources in the Docker plugin sweep", () => {
const sweep = readFileSync(PLUGINS_DOCKER_SWEEP_PATH, "utf8");
const clawhub = readFileSync(PLUGINS_DOCKER_CLAWHUB_PATH, "utf8");
const assertions = readFileSync(PLUGINS_DOCKER_ASSERTIONS_PATH, "utf8");
const npmRegistry = readFileSync(PLUGINS_DOCKER_NPM_REGISTRY_PATH, "utf8");
expect(sweep).toContain('plugins install "$dir_plugin"');
expect(sweep).toContain("plugins update demo-plugin-dir");
expect(assertions).toContain('Skipping "demo-plugin-dir" (source: path).');
expect(sweep).toContain("start_npm_fixture_registry");
expect(sweep).toContain('plugins install "npm:@openclaw/demo-plugin-npm@0.0.1"');
expect(sweep).toContain("plugins update demo-plugin-npm");
expect(assertions).toContain("demo-plugin-npm is up to date (0.0.1).");
expect(npmRegistry).toContain('"dist-tags": { latest: entry.version }');
expect(npmRegistry).toContain("packageArgs.length % 3");
expect(sweep).toContain('plugins install "git:$git_update_repo_url@main"');
expect(sweep).toContain("plugins update demo-plugin-git-update");
expect(assertions).toContain("demo.git.update.v2");
expect(clawhub).toContain('plugins install "$CLAWHUB_PLUGIN_SPEC"');
expect(clawhub).toContain('plugins update "$CLAWHUB_PLUGIN_ID"');
expect(assertions).toContain("clawhub-updated");
});
});