#!/usr/bin/env bash # Verifies `openclaw plugins update` is a no-op for an already-current plugin. # The CLI under test is installed from the prepared npm tarball in a bare runner. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugin-update-e2e" OPENCLAW_PLUGIN_UPDATE_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_PLUGIN_UPDATE_E2E_SKIP_BUILD:-0}" PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz plugin-update "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" # Bare lanes mount the package artifact instead of baking app sources into the image. docker_e2e_package_mount_args "$PACKAGE_TGZ" docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD" OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 plugin-update empty)" echo "Running unchanged plugin update smoke..." docker run --rm \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ -e OPENCLAW_SKIP_CHANNELS=1 \ -e OPENCLAW_SKIP_PROVIDERS=1 \ -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ "$IMAGE_NAME" \ bash -lc "set -euo pipefail eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\" package_tgz=\"\${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}\" npm install -g --prefix /tmp/npm-prefix \"\$package_tgz\" --no-fund --no-audit >/tmp/openclaw-install.log 2>&1 entry=\"/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.mjs\" [ -f \"\$entry\" ] || entry=/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.js package_version=\$(node -p \"require('/tmp/npm-prefix/lib/node_modules/openclaw/package.json').version\") OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT=\$(PACKAGE_VERSION=\"\$package_version\" node -e 'const version = process.env.PACKAGE_VERSION || \"\"; const match = new RegExp(\"^(\\\\d{4})\\\\.(\\\\d{1,2})\\\\.(\\\\d{1,2})(?:[-+].*)?\").exec(version); if (!match) { console.log(\"0\"); process.exit(0); } const value = [Number(match[1]), Number(match[2]), Number(match[3])]; const max = [2026, 4, 25]; for (let i = 0; i < value.length; i += 1) { if (value[i] < max[i]) { console.log(\"1\"); process.exit(0); } if (value[i] > max[i]) { console.log(\"0\"); process.exit(0); } } console.log(\"1\");') export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT export NPM_CONFIG_REGISTRY=http://127.0.0.1:4873 export PATH=\"/tmp/npm-prefix/bin:\$PATH\" mkdir -p \"\$HOME/.openclaw/extensions/lossless-claw\" cat > \"\$HOME/.openclaw/extensions/lossless-claw/package.json\" <<'JSON' { \"name\": \"@example/lossless-claw\", \"version\": \"0.9.0\" } JSON cat > \"\$OPENCLAW_CONFIG_PATH\" <<'JSON' { \"plugins\": {} } JSON mkdir -p \"\$HOME/.openclaw/plugins\" cat > \"\$HOME/.openclaw/plugins/installs.json\" <<'JSON' { \"version\": 1, \"warning\": \"DO NOT EDIT. This file is generated by OpenClaw plugin registry commands.\", \"hostContractVersion\": \"docker-e2e\", \"compatRegistryVersion\": \"docker-e2e\", \"migrationVersion\": 1, \"policyHash\": \"docker-e2e\", \"generatedAtMs\": 1777118400000, \"installRecords\": { \"lossless-claw\": { \"source\": \"npm\", \"spec\": \"@example/lossless-claw@0.9.0\", \"installPath\": \"~/.openclaw/extensions/lossless-claw\", \"resolvedName\": \"@example/lossless-claw\", \"resolvedVersion\": \"0.9.0\", \"resolvedSpec\": \"@example/lossless-claw@0.9.0\", \"integrity\": \"sha512-same\", \"shasum\": \"same\" } }, \"plugins\": [], \"diagnostics\": [] } JSON cat > /tmp/openclaw-e2e-registry.mjs <<'NODE' import http from 'node:http'; const metadata = { name: '@example/lossless-claw', 'dist-tags': { latest: '0.9.0' }, versions: { '0.9.0': { name: '@example/lossless-claw', version: '0.9.0', dist: { integrity: 'sha512-same', shasum: 'same', tarball: 'http://127.0.0.1:4873/@example/lossless-claw/-/lossless-claw-0.9.0.tgz' } } } }; const server = http.createServer((req, res) => { if (req.url === '/@example%2flossless-claw' || req.url === '/@example%2Flossless-claw') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(metadata)); return; } res.writeHead(404, { 'content-type': 'text/plain' }); res.end('not found: ' + req.url); }); server.listen(4873, '127.0.0.1'); NODE node /tmp/openclaw-e2e-registry.mjs >/tmp/openclaw-e2e-registry.log 2>&1 & registry_pid=\$! trap 'kill \"\$registry_pid\" >/dev/null 2>&1 || true' EXIT registry_ready=0 for _ in \$(seq 1 50); do if node --input-type=module -e ' import http from \"node:http\"; const req = http.get(\"http://127.0.0.1:4873/@example%2flossless-claw\", (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on(\"error\", () => process.exit(1)); req.setTimeout(200, () => { req.destroy(); process.exit(1); }); '; then registry_ready=1 break fi sleep 0.1 done if [ \"\$registry_ready\" -ne 1 ]; then echo \"Local npm metadata registry failed to start\" cat /tmp/openclaw-e2e-registry.log || true exit 1 fi before_config_hash=\"\" if [ \"\$OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT\" != \"1\" ]; then before_config_hash=\$(sha256sum \"\$OPENCLAW_CONFIG_PATH\" | awk '{print \$1}') fi plugin_update_timeout_seconds=\"\${OPENCLAW_PLUGIN_UPDATE_TIMEOUT_SECONDS:-180}\" node --input-type=module > /tmp/plugin-update-before.json <<'NODE' import fs from \"node:fs\"; import os from \"node:os\"; import path from \"node:path\"; const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, \"utf8\")); } catch { return {}; } }; const home = os.homedir(); const config = readJson(path.join(home, \".openclaw\", \"openclaw.json\")); const index = readJson(path.join(home, \".openclaw\", \"plugins\", \"installs.json\")); const records = index.installRecords ?? index.records ?? config.plugins?.installs ?? {}; const record = records[\"lossless-claw\"] ?? records[\"@example/lossless-claw\"]; if (!record) { throw new Error(\"missing seeded plugin install record\"); } const snapshot = { source: record.source, spec: record.spec, resolvedName: record.resolvedName, resolvedVersion: record.resolvedVersion, resolvedSpec: record.resolvedSpec, integrity: record.integrity, shasum: record.shasum }; process.stdout.write(JSON.stringify(snapshot, null, 2)); NODE set +e timeout \"\${plugin_update_timeout_seconds}s\" node \"\$entry\" plugins update @example/lossless-claw > /tmp/plugin-update-output.log 2>&1 plugin_update_status=\$? set -e if [ \"\$plugin_update_status\" -ne 0 ]; then echo \"Plugin update command failed or timed out after \${plugin_update_timeout_seconds}s (status \${plugin_update_status})\" echo \"--- plugin update output ---\" cat /tmp/plugin-update-output.log || true echo \"--- local registry output ---\" cat /tmp/openclaw-e2e-registry.log || true exit \"\$plugin_update_status\" fi if [ -n \"\$before_config_hash\" ]; then after_config_hash=\$(sha256sum \"\$OPENCLAW_CONFIG_PATH\" | awk '{print \$1}') if [ \"\$before_config_hash\" != \"\$after_config_hash\" ]; then echo \"Config changed unexpectedly for modern package \$package_version\" cat /tmp/plugin-update-output.log exit 1 fi fi node --input-type=module <<'NODE' import fs from \"node:fs\"; import os from \"node:os\"; import path from \"node:path\"; const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, \"utf8\")); } catch { return {}; } }; const home = os.homedir(); const before = readJson(\"/tmp/plugin-update-before.json\"); const config = readJson(path.join(home, \".openclaw\", \"openclaw.json\")); const index = readJson(path.join(home, \".openclaw\", \"plugins\", \"installs.json\")); const records = index.installRecords ?? index.records ?? config.plugins?.installs ?? {}; const record = records[\"lossless-claw\"] ?? records[\"@example/lossless-claw\"]; if (!record) { throw new Error(\"missing plugin install record after update\"); } const after = { source: record.source, spec: record.spec, resolvedName: record.resolvedName, resolvedVersion: record.resolvedVersion, resolvedSpec: record.resolvedSpec, integrity: record.integrity, shasum: record.shasum }; if (JSON.stringify(before) !== JSON.stringify(after)) { throw new Error(\"plugin install record changed unexpectedly: \" + JSON.stringify({ before, after })); } NODE if grep -q 'Downloading @example/lossless-claw' /tmp/plugin-update-output.log; then echo \"Unexpected npm download/reinstall path\" cat /tmp/plugin-update-output.log exit 1 fi if ! grep -q 'lossless-claw is up to date (0.9.0).' /tmp/plugin-update-output.log; then echo \"Expected up-to-date output missing\" cat /tmp/plugin-update-output.log exit 1 fi cat /tmp/plugin-update-output.log " echo "Plugin update unchanged Docker E2E passed."