From 64387ad8e2cc504dcb727af3bec882057f6452d8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 08:03:15 +0100 Subject: [PATCH] refactor: simplify docker e2e harness scripts --- ...bundled-plugin-install-uninstall-docker.sh | 202 +--------------- scripts/e2e/lib/bundled-channel/channel.sh | 35 +-- scripts/e2e/lib/bundled-channel/common.sh | 10 +- .../probe.mjs | 157 +++++++++++++ .../bundled-plugin-install-uninstall/sweep.sh | 51 ++++ .../e2e/lib/plugin-update/registry-server.mjs | 29 +++ .../lib/plugin-update/unchanged-scenario.sh | 190 +++++++++++++++ .../e2e/npm-onboard-channel-agent-docker.sh | 31 +-- scripts/e2e/plugin-update-unchanged-docker.sh | 219 +----------------- scripts/lib/openclaw-e2e-instance.sh | 97 ++++++++ 10 files changed, 542 insertions(+), 479 deletions(-) create mode 100644 scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs create mode 100644 scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh create mode 100644 scripts/e2e/lib/plugin-update/registry-server.mjs create mode 100644 scripts/e2e/lib/plugin-update/unchanged-scenario.sh diff --git a/scripts/e2e/bundled-plugin-install-uninstall-docker.sh b/scripts/e2e/bundled-plugin-install-uninstall-docker.sh index 423c9b049f5..660267de7aa 100755 --- a/scripts/e2e/bundled-plugin-install-uninstall-docker.sh +++ b/scripts/e2e/bundled-plugin-install-uninstall-docker.sh @@ -7,6 +7,7 @@ IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-bundled-plugin-install-uninstal docker_e2e_build_or_reuse "$IMAGE_NAME" bundled-plugin-install-uninstall OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 bundled-plugin-install-uninstall empty)" +docker_e2e_harness_mount_args DOCKER_ENV_ARGS=( -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 @@ -24,202 +25,11 @@ done echo "Running bundled plugin install/uninstall Docker E2E..." RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-bundled-plugin-install-uninstall.XXXXXX")" -if ! docker run --rm "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash -s >"$RUN_LOG" 2>&1 <<'EOF' -set -euo pipefail - -if [ -f dist/index.mjs ]; then - OPENCLAW_ENTRY="dist/index.mjs" -elif [ -f dist/index.js ]; then - OPENCLAW_ENTRY="dist/index.js" -else - echo "Missing dist/index.(m)js (build output):" - ls -la dist || true - exit 1 -fi -export OPENCLAW_ENTRY - -eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)" - -node - <<'NODE' > /tmp/bundled-plugin-sweep-ids -const fs = require("node:fs"); -const path = require("node:path"); - -const explicit = (process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "") - .split(/[,\s]+/u) - .map((entry) => entry.trim()) - .filter(Boolean); -const extensionRoot = path.join(process.cwd(), "dist", "extensions"); -const manifestEntries = fs - .readdirSync(extensionRoot, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => { - const manifestPath = path.join(extensionRoot, entry.name, "openclaw.plugin.json"); - if (!fs.existsSync(manifestPath)) { - return null; - } - const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - const id = typeof manifest.id === "string" ? manifest.id.trim() : ""; - if (!id) { - throw new Error(`Bundled plugin manifest is missing id: ${manifestPath}`); - } - const required = manifest.configSchema?.required; - return { - id, - dir: entry.name, - requiresConfig: - Array.isArray(required) && required.some((value) => typeof value === "string"), - }; - }) - .filter(Boolean) - .sort((a, b) => a.id.localeCompare(b.id)); -const allEntries = - explicit.length > 0 - ? explicit.map( - (lookup) => - manifestEntries.find((entry) => entry.id === lookup || entry.dir === lookup) || { - id: lookup, - dir: lookup, - requiresConfig: false, - }, - ) - : manifestEntries; - -const total = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL || "1", 10); -const index = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX || "0", 10); -if (!Number.isInteger(total) || total < 1) { - throw new Error(`OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL must be >= 1, got ${process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL}`); -} -if (!Number.isInteger(index) || index < 0 || index >= total) { - throw new Error(`OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX must be in [0, ${total - 1}], got ${process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX}`); -} - -const selected = allEntries.filter((_, candidateIndex) => candidateIndex % total === index); -if (selected.length === 0) { - throw new Error(`No bundled plugin ids selected for shard ${index}/${total}`); -} - -for (const entry of selected) { - console.log(`${entry.id}\t${entry.dir}\t${entry.requiresConfig ? "1" : "0"}`); -} -NODE - -mapfile -t plugin_entries < /tmp/bundled-plugin-sweep-ids -selected_labels=() -for plugin_entry in "${plugin_entries[@]}"; do - IFS=$'\t' read -r plugin_id plugin_dir _requires_config <<<"$plugin_entry" - selected_labels+=("${plugin_id}@${plugin_dir}") -done -echo "Selected ${#plugin_entries[@]} bundled plugins for shard ${OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX:-0}/${OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL:-1}: ${selected_labels[*]}" - -assert_installed() { - local plugin_id="$1" - local plugin_dir="$2" - local requires_config="$3" - node - <<'NODE' "$plugin_id" "$plugin_dir" "$requires_config" -const fs = require("node:fs"); -const path = require("node:path"); - -const pluginId = process.argv[2]; -const pluginDir = process.argv[3]; -const requiresConfig = process.argv[4] === "1"; -const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); -const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); -const config = JSON.parse(fs.readFileSync(configPath, "utf8")); -const index = JSON.parse(fs.readFileSync(indexPath, "utf8")); -const records = index.installRecords ?? index.records ?? {}; -const record = records[pluginId]; -if (!record) { - throw new Error(`missing install record for ${pluginId}`); -} -if (record.source !== "path") { - throw new Error(`expected bundled install record source=path for ${pluginId}, got ${record.source}`); -} -if (typeof record.sourcePath !== "string" || !record.sourcePath.includes(`/dist/extensions/${pluginDir}`)) { - throw new Error(`unexpected bundled source path for ${pluginId}: ${record.sourcePath}`); -} -if (record.installPath !== record.sourcePath) { - throw new Error(`bundled install path should equal source path for ${pluginId}`); -} -const paths = config.plugins?.load?.paths || []; -if (paths.some((entry) => String(entry).includes(`/dist/extensions/${pluginDir}`))) { - throw new Error(`config load paths should not include bundled install path for ${pluginId}`); -} -if (requiresConfig && config.plugins?.entries?.[pluginId]?.enabled === true) { - throw new Error(`plugin requiring config should not be enabled immediately after install for ${pluginId}`); -} -if (!requiresConfig && config.plugins?.entries?.[pluginId]?.enabled !== true) { - throw new Error(`config entry is not enabled after install for ${pluginId}`); -} -const allow = config.plugins?.allow || []; -if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) { - throw new Error(`existing allowlist does not include ${pluginId} after install`); -} -if ((config.plugins?.deny || []).includes(pluginId)) { - throw new Error(`denylist contains ${pluginId} after install`); -} -NODE -} - -assert_uninstalled() { - local plugin_id="$1" - local plugin_dir="$2" - node - <<'NODE' "$plugin_id" "$plugin_dir" -const fs = require("node:fs"); -const path = require("node:path"); - -const pluginId = process.argv[2]; -const pluginDir = process.argv[3]; -const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); -const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); -const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {}; -const index = fs.existsSync(indexPath) ? JSON.parse(fs.readFileSync(indexPath, "utf8")) : {}; -const records = index.installRecords ?? index.records ?? {}; -if (records[pluginId]) { - throw new Error(`install record still present after uninstall for ${pluginId}`); -} -const paths = config.plugins?.load?.paths || []; -if (paths.some((entry) => String(entry).includes(`/dist/extensions/${pluginDir}`))) { - throw new Error(`load path still present after uninstall for ${pluginId}`); -} -if (config.plugins?.entries?.[pluginId]) { - throw new Error(`config entry still present after uninstall for ${pluginId}`); -} -if ((config.plugins?.allow || []).includes(pluginId)) { - throw new Error(`allowlist still contains ${pluginId} after uninstall`); -} -if ((config.plugins?.deny || []).includes(pluginId)) { - throw new Error(`denylist still contains ${pluginId} after uninstall`); -} -const managedPath = path.join(process.env.HOME, ".openclaw", "extensions", pluginId); -if (fs.existsSync(managedPath)) { - throw new Error(`managed install directory unexpectedly exists for bundled plugin ${pluginId}: ${managedPath}`); -} -NODE -} - -plugin_index=0 -for plugin_entry in "${plugin_entries[@]}"; do - IFS=$'\t' read -r plugin_id plugin_dir requires_config <<<"$plugin_entry" - install_log="/tmp/openclaw-install-${plugin_index}.log" - uninstall_log="/tmp/openclaw-uninstall-${plugin_index}.log" - echo "Installing bundled plugin: $plugin_id ($plugin_dir)" - node "$OPENCLAW_ENTRY" plugins install "$plugin_id" >"$install_log" 2>&1 || { - cat "$install_log" - exit 1 - } - assert_installed "$plugin_id" "$plugin_dir" "$requires_config" - - echo "Uninstalling bundled plugin: $plugin_id ($plugin_dir)" - node "$OPENCLAW_ENTRY" plugins uninstall "$plugin_id" --force >"$uninstall_log" 2>&1 || { - cat "$uninstall_log" - exit 1 - } - assert_uninstalled "$plugin_id" "$plugin_dir" - plugin_index=$((plugin_index + 1)) -done - -echo "bundled plugin install/uninstall sweep passed (${#plugin_entries[@]} plugin(s))" -EOF +if ! docker run --rm \ + "${DOCKER_ENV_ARGS[@]}" \ + "${DOCKER_E2E_HARNESS_ARGS[@]}" \ + "$IMAGE_NAME" \ + bash scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh >"$RUN_LOG" 2>&1 then cat "$RUN_LOG" rm -f "$RUN_LOG" diff --git a/scripts/e2e/lib/bundled-channel/channel.sh b/scripts/e2e/lib/bundled-channel/channel.sh index dc9acc33eba..4fcb885574c 100644 --- a/scripts/e2e/lib/bundled-channel/channel.sh +++ b/scripts/e2e/lib/bundled-channel/channel.sh @@ -35,32 +35,7 @@ DEP_SENTINEL="${OPENCLAW_DEP_SENTINEL:?missing OPENCLAW_DEP_SENTINEL}" gateway_pid="" terminate_gateways() { - if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then - kill "$gateway_pid" 2>/dev/null || true - fi - if command -v pkill >/dev/null 2>&1; then - pkill -TERM -f "[o]penclaw-gateway" 2>/dev/null || true - fi - for _ in $(seq 1 100); do - local alive=0 - if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then - alive=1 - fi - if command -v pgrep >/dev/null 2>&1 && pgrep -f "[o]penclaw-gateway" >/dev/null 2>&1; then - alive=1 - fi - [ "$alive" = "0" ] && break - sleep 0.1 - done - if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then - kill -KILL "$gateway_pid" 2>/dev/null || true - fi - if command -v pkill >/dev/null 2>&1; then - pkill -KILL -f "[o]penclaw-gateway" 2>/dev/null || true - fi - if [ -n "${gateway_pid:-}" ]; then - wait "$gateway_pid" 2>/dev/null || true - fi + openclaw_e2e_terminate_gateways "${gateway_pid:-}" } cleanup() { @@ -71,12 +46,8 @@ trap cleanup EXIT bundled_channel_install_package /tmp/openclaw-install.log command -v openclaw >/dev/null -package_root="$(npm root -g)/openclaw" -test -d "$package_root/dist/extensions/telegram" -test -d "$package_root/dist/extensions/discord" -test -d "$package_root/dist/extensions/slack" -test -d "$package_root/dist/extensions/feishu" -test -d "$package_root/dist/extensions/memory-lancedb" +package_root="$(openclaw_e2e_package_root)" +openclaw_e2e_assert_package_extensions "$package_root" telegram discord slack feishu memory-lancedb if [ -d "$package_root/dist/extensions/$CHANNEL/node_modules" ]; then echo "$CHANNEL runtime deps should not be preinstalled in package" >&2 diff --git a/scripts/e2e/lib/bundled-channel/common.sh b/scripts/e2e/lib/bundled-channel/common.sh index ca82941ba09..95c029f02bc 100644 --- a/scripts/e2e/lib/bundled-channel/common.sh +++ b/scripts/e2e/lib/bundled-channel/common.sh @@ -17,15 +17,7 @@ bundled_channel_stage_dir() { } bundled_channel_install_package() { - local log_file="$1" - local label="${2:-mounted OpenClaw package}" - local package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}" - echo "Installing $label..." - if ! npm install -g "$package_tgz" --no-fund --no-audit >"$log_file" 2>&1; then - echo "npm install -g failed for $label" >&2 - cat "$log_file" >&2 || true - exit 1 - fi + openclaw_e2e_install_package "$@" } bundled_channel_find_external_dep_package() { diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs b/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs new file mode 100644 index 00000000000..af131fe73a9 --- /dev/null +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs @@ -0,0 +1,157 @@ +import fs from "node:fs"; +import path from "node:path"; + +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +function loadManifestEntries() { + const explicit = (process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "") + .split(/[,\s]+/u) + .map((entry) => entry.trim()) + .filter(Boolean); + const extensionRoot = path.join(process.cwd(), "dist", "extensions"); + const manifestEntries = fs + .readdirSync(extensionRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => { + const manifestPath = path.join(extensionRoot, entry.name, "openclaw.plugin.json"); + if (!fs.existsSync(manifestPath)) { + return null; + } + const manifest = readJson(manifestPath); + const id = typeof manifest.id === "string" ? manifest.id.trim() : ""; + if (!id) { + throw new Error(`Bundled plugin manifest is missing id: ${manifestPath}`); + } + const required = manifest.configSchema?.required; + return { + id, + dir: entry.name, + requiresConfig: + Array.isArray(required) && required.some((value) => typeof value === "string"), + }; + }) + .filter(Boolean) + .toSorted((a, b) => a.id.localeCompare(b.id)); + + if (explicit.length === 0) { + return manifestEntries; + } + return explicit.map( + (lookup) => + manifestEntries.find((entry) => entry.id === lookup || entry.dir === lookup) || { + id: lookup, + dir: lookup, + requiresConfig: false, + }, + ); +} + +function selectedManifestEntries() { + const allEntries = loadManifestEntries(); + const total = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL || "1", 10); + const index = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX || "0", 10); + if (!Number.isInteger(total) || total < 1) { + throw new Error( + `OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL must be >= 1, got ${process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL}`, + ); + } + if (!Number.isInteger(index) || index < 0 || index >= total) { + throw new Error( + `OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX must be in [0, ${total - 1}], got ${process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX}`, + ); + } + + const selected = allEntries.filter((_, candidateIndex) => candidateIndex % total === index); + if (selected.length === 0) { + throw new Error(`No bundled plugin ids selected for shard ${index}/${total}`); + } + return selected; +} + +function assertInstalled(pluginId, pluginDir, requiresConfig) { + const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); + const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); + const config = readJson(configPath); + const index = readJson(indexPath); + const records = index.installRecords ?? index.records ?? {}; + const record = records[pluginId]; + if (!record) { + throw new Error(`missing install record for ${pluginId}`); + } + if (record.source !== "path") { + throw new Error( + `expected bundled install record source=path for ${pluginId}, got ${record.source}`, + ); + } + if ( + typeof record.sourcePath !== "string" || + !record.sourcePath.includes(`/dist/extensions/${pluginDir}`) + ) { + throw new Error(`unexpected bundled source path for ${pluginId}: ${record.sourcePath}`); + } + if (record.installPath !== record.sourcePath) { + throw new Error(`bundled install path should equal source path for ${pluginId}`); + } + const paths = config.plugins?.load?.paths || []; + if (paths.some((entry) => String(entry).includes(`/dist/extensions/${pluginDir}`))) { + throw new Error(`config load paths should not include bundled install path for ${pluginId}`); + } + if (requiresConfig && config.plugins?.entries?.[pluginId]?.enabled === true) { + throw new Error( + `plugin requiring config should not be enabled immediately after install for ${pluginId}`, + ); + } + if (!requiresConfig && config.plugins?.entries?.[pluginId]?.enabled !== true) { + throw new Error(`config entry is not enabled after install for ${pluginId}`); + } + const allow = config.plugins?.allow || []; + if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) { + throw new Error(`existing allowlist does not include ${pluginId} after install`); + } + if ((config.plugins?.deny || []).includes(pluginId)) { + throw new Error(`denylist contains ${pluginId} after install`); + } +} + +function assertUninstalled(pluginId, pluginDir) { + const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); + const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); + const config = fs.existsSync(configPath) ? readJson(configPath) : {}; + const index = fs.existsSync(indexPath) ? readJson(indexPath) : {}; + const records = index.installRecords ?? index.records ?? {}; + if (records[pluginId]) { + throw new Error(`install record still present after uninstall for ${pluginId}`); + } + const paths = config.plugins?.load?.paths || []; + if (paths.some((entry) => String(entry).includes(`/dist/extensions/${pluginDir}`))) { + throw new Error(`load path still present after uninstall for ${pluginId}`); + } + if (config.plugins?.entries?.[pluginId]) { + throw new Error(`config entry still present after uninstall for ${pluginId}`); + } + if ((config.plugins?.allow || []).includes(pluginId)) { + throw new Error(`allowlist still contains ${pluginId} after uninstall`); + } + if ((config.plugins?.deny || []).includes(pluginId)) { + throw new Error(`denylist still contains ${pluginId} after uninstall`); + } + const managedPath = path.join(process.env.HOME, ".openclaw", "extensions", pluginId); + if (fs.existsSync(managedPath)) { + throw new Error( + `managed install directory unexpectedly exists for bundled plugin ${pluginId}: ${managedPath}`, + ); + } +} + +const [command, pluginId, pluginDir, requiresConfig] = process.argv.slice(2); +if (command === "select") { + for (const entry of selectedManifestEntries()) { + console.log(`${entry.id}\t${entry.dir}\t${entry.requiresConfig ? "1" : "0"}`); + } +} else if (command === "assert-installed") { + assertInstalled(pluginId, pluginDir, requiresConfig === "1"); +} else if (command === "assert-uninstalled") { + assertUninstalled(pluginId, pluginDir); +} else { + throw new Error(`Unknown bundled plugin probe command: ${command || "(missing)"}`); +} diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh b/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh new file mode 100644 index 00000000000..1c03b529970 --- /dev/null +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +source scripts/lib/openclaw-e2e-instance.sh + +if [ -f dist/index.mjs ]; then + OPENCLAW_ENTRY="dist/index.mjs" +elif [ -f dist/index.js ]; then + OPENCLAW_ENTRY="dist/index.js" +else + echo "Missing dist/index.(m)js (build output):" + ls -la dist || true + exit 1 +fi +export OPENCLAW_ENTRY + +openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" + +probe="scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs" +node "$probe" select > /tmp/bundled-plugin-sweep-ids + +mapfile -t plugin_entries < /tmp/bundled-plugin-sweep-ids +selected_labels=() +for plugin_entry in "${plugin_entries[@]}"; do + IFS=$'\t' read -r plugin_id plugin_dir _requires_config <<<"$plugin_entry" + selected_labels+=("${plugin_id}@${plugin_dir}") +done +echo "Selected ${#plugin_entries[@]} bundled plugins for shard ${OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX:-0}/${OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL:-1}: ${selected_labels[*]}" + +plugin_index=0 +for plugin_entry in "${plugin_entries[@]}"; do + IFS=$'\t' read -r plugin_id plugin_dir requires_config <<<"$plugin_entry" + install_log="/tmp/openclaw-install-${plugin_index}.log" + uninstall_log="/tmp/openclaw-uninstall-${plugin_index}.log" + echo "Installing bundled plugin: $plugin_id ($plugin_dir)" + node "$OPENCLAW_ENTRY" plugins install "$plugin_id" >"$install_log" 2>&1 || { + cat "$install_log" + exit 1 + } + node "$probe" assert-installed "$plugin_id" "$plugin_dir" "$requires_config" + + echo "Uninstalling bundled plugin: $plugin_id ($plugin_dir)" + node "$OPENCLAW_ENTRY" plugins uninstall "$plugin_id" --force >"$uninstall_log" 2>&1 || { + cat "$uninstall_log" + exit 1 + } + node "$probe" assert-uninstalled "$plugin_id" "$plugin_dir" + plugin_index=$((plugin_index + 1)) +done + +echo "bundled plugin install/uninstall sweep passed (${#plugin_entries[@]} plugin(s))" diff --git a/scripts/e2e/lib/plugin-update/registry-server.mjs b/scripts/e2e/lib/plugin-update/registry-server.mjs new file mode 100644 index 00000000000..5c33649d54d --- /dev/null +++ b/scripts/e2e/lib/plugin-update/registry-server.mjs @@ -0,0 +1,29 @@ +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"); diff --git a/scripts/e2e/lib/plugin-update/unchanged-scenario.sh b/scripts/e2e/lib/plugin-update/unchanged-scenario.sh new file mode 100644 index 00000000000..2f8c0f0c455 --- /dev/null +++ b/scripts/e2e/lib/plugin-update/unchanged-scenario.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +set -euo pipefail + +source scripts/lib/openclaw-e2e-instance.sh + +openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" +openclaw_e2e_install_package /tmp/openclaw-install.log "mounted OpenClaw package" /tmp/npm-prefix + +package_root="$(openclaw_e2e_package_root /tmp/npm-prefix)" +entry="$(openclaw_e2e_package_entrypoint "$package_root")" +package_version="$(node -p "require('$package_root/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 + +node scripts/e2e/lib/plugin-update/registry-server.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 diff --git a/scripts/e2e/npm-onboard-channel-agent-docker.sh b/scripts/e2e/npm-onboard-channel-agent-docker.sh index 77c85ac7968..0d87f53bd00 100644 --- a/scripts/e2e/npm-onboard-channel-agent-docker.sh +++ b/scripts/e2e/npm-onboard-channel-agent-docker.sh @@ -103,32 +103,11 @@ dump_debug_logs() { } trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR -echo "Installing mounted OpenClaw package..." -package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}" -npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-install.log 2>&1 +openclaw_e2e_install_package /tmp/openclaw-install.log command -v openclaw >/dev/null -package_root="$(npm root -g)/openclaw" -test -d "$package_root/dist/extensions/telegram" -test -d "$package_root/dist/extensions/discord" - -assert_dep_absent() { - local sentinel="$1" - if find "$package_root" "$HOME/.openclaw" -path "*/node_modules/$sentinel/package.json" -print -quit 2>/dev/null | grep -q .; then - echo "$sentinel should not be installed before channel activation repair" >&2 - find "$package_root" "$HOME/.openclaw" -path "*/node_modules/$sentinel/package.json" -print 2>/dev/null >&2 || true - exit 1 - fi -} - -assert_dep_present() { - local sentinel="$1" - if ! find "$package_root" "$HOME/.openclaw" -path "*/node_modules/$sentinel/package.json" -print -quit 2>/dev/null | grep -q .; then - echo "$sentinel was not installed on demand" >&2 - find "$package_root" "$HOME/.openclaw" -maxdepth 6 -type d -name node_modules -print 2>/dev/null >&2 || true - exit 1 - fi -} +package_root="$(openclaw_e2e_package_root)" +openclaw_e2e_assert_package_extensions "$package_root" telegram discord mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" /tmp/openclaw-mock-openai.log)" openclaw_e2e_wait_mock_openai "$MOCK_PORT" @@ -229,7 +208,7 @@ cfg.plugins = { fs.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`); NODE -assert_dep_absent "$DEP_SENTINEL" +openclaw_e2e_assert_dep_absent "$DEP_SENTINEL" "$package_root" "$HOME/.openclaw" echo "Configuring $CHANNEL..." openclaw channels add --channel "$CHANNEL" --token "$CHANNEL_TOKEN" >/tmp/openclaw-channel-add.log 2>&1 @@ -251,7 +230,7 @@ NODE echo "Running doctor after channel activation..." openclaw doctor --repair --non-interactive >/tmp/openclaw-doctor.log 2>&1 -assert_dep_present "$DEP_SENTINEL" +openclaw_e2e_assert_dep_present "$DEP_SENTINEL" "$package_root" "$HOME/.openclaw" echo "Running local agent turn against mocked OpenAI..." openclaw agent --local \ diff --git a/scripts/e2e/plugin-update-unchanged-docker.sh b/scripts/e2e/plugin-update-unchanged-docker.sh index 353dc2abeca..386bef49a20 100755 --- a/scripts/e2e/plugin-update-unchanged-docker.sh +++ b/scripts/e2e/plugin-update-unchanged-docker.sh @@ -12,6 +12,7 @@ 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_harness_mount_args 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)" @@ -23,222 +24,8 @@ docker run --rm \ -e OPENCLAW_SKIP_PROVIDERS=1 \ -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + "${DOCKER_E2E_HARNESS_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 -" + bash scripts/e2e/lib/plugin-update/unchanged-scenario.sh echo "Plugin update unchanged Docker E2E passed." diff --git a/scripts/lib/openclaw-e2e-instance.sh b/scripts/lib/openclaw-e2e-instance.sh index 05b8bcad07e..28bdc013349 100644 --- a/scripts/lib/openclaw-e2e-instance.sh +++ b/scripts/lib/openclaw-e2e-instance.sh @@ -9,6 +9,74 @@ openclaw_e2e_resolve_entrypoint() { echo "OpenClaw entrypoint not found under dist/" >&2 return 1 } +openclaw_e2e_package_root() { + local prefix="${1:-}" + if [ -n "$prefix" ]; then + printf '%s/lib/node_modules/openclaw\n' "$prefix" + return 0 + fi + printf '%s/openclaw\n' "$(npm root -g)" +} +openclaw_e2e_package_entrypoint() { + local root="${1:?missing package root}" + local entry + for entry in "$root/dist/index.mjs" "$root/dist/index.js"; do + [ -f "$entry" ] && { printf '%s\n' "$entry"; return 0; } + done + echo "OpenClaw package entrypoint not found under $root/dist/" >&2 + return 1 +} +openclaw_e2e_install_package() { + local log_file="$1" + local label="${2:-mounted OpenClaw package}" + local prefix="${3:-}" + local package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}" + local args=(-g) + if [ -n "$prefix" ]; then + args+=("--prefix" "$prefix") + fi + echo "Installing $label..." + if ! npm install "${args[@]}" "$package_tgz" --no-fund --no-audit >"$log_file" 2>&1; then + echo "npm install failed for $label" >&2 + cat "$log_file" >&2 || true + exit 1 + fi +} +openclaw_e2e_assert_package_extensions() { + local root="$1" + shift + local extension + for extension in "$@"; do + [ -d "$root/dist/extensions/$extension" ] || { + echo "Missing packaged extension: $extension" >&2 + exit 1 + } + done +} +openclaw_e2e_find_dep_package() { + local dep_path="$1" + shift + find "$@" -path "*/node_modules/$dep_path/package.json" -print -quit 2>/dev/null || true +} +openclaw_e2e_assert_dep_absent() { + local dep_path="$1" + shift + if [ -n "$(openclaw_e2e_find_dep_package "$dep_path" "$@")" ]; then + echo "$dep_path should not be installed" >&2 + find "$@" -path "*/node_modules/$dep_path/package.json" -print 2>/dev/null >&2 || true + exit 1 + fi +} +openclaw_e2e_assert_dep_present() { + local dep_path="$1" + shift + if [ -n "$(openclaw_e2e_find_dep_package "$dep_path" "$@")" ]; then + return 0 + fi + echo "$dep_path was not installed on demand" >&2 + find "$@" -maxdepth 6 -type d -name node_modules -print 2>/dev/null >&2 || true + exit 1 +} openclaw_e2e_write_state_env() { local target="${1:-/tmp/openclaw-test-state-env}" { @@ -49,6 +117,35 @@ openclaw_e2e_stop_process() { kill -9 "$pid" >/dev/null 2>&1 || true wait "$pid" >/dev/null 2>&1 || true } +openclaw_e2e_terminate_gateways() { + local pid="${1:-}" _ + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + fi + if command -v pkill >/dev/null 2>&1; then + pkill -TERM -f "[o]penclaw-gateway" 2>/dev/null || true + fi + for _ in $(seq 1 100); do + local alive=0 + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + alive=1 + fi + if command -v pgrep >/dev/null 2>&1 && pgrep -f "[o]penclaw-gateway" >/dev/null 2>&1; then + alive=1 + fi + [ "$alive" = "0" ] && break + sleep 0.1 + done + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill -KILL "$pid" 2>/dev/null || true + fi + if command -v pkill >/dev/null 2>&1; then + pkill -KILL -f "[o]penclaw-gateway" 2>/dev/null || true + fi + if [ -n "$pid" ]; then + wait "$pid" 2>/dev/null || true + fi +} openclaw_e2e_start_mock_openai() { MOCK_PORT="$1" node scripts/e2e/mock-openai-server.mjs >"$2" 2>&1 & printf '%s\n' "$!"; } openclaw_e2e_wait_mock_openai() { local port="$1" attempts="${2:-80}" _