mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
test: broaden plugin install update coverage
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
99
scripts/e2e/lib/plugins/npm-registry-server.mjs
Normal file
99
scripts/e2e/lib/plugins/npm-registry-server.mjs
Normal 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));
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user