refactor: simplify docker e2e harness scripts

This commit is contained in:
Peter Steinberger
2026-04-29 08:03:15 +01:00
parent e71d7d48fb
commit 64387ad8e2
10 changed files with 542 additions and 479 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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() {

View File

@@ -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)"}`);
}

View File

@@ -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))"

View File

@@ -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");

View File

@@ -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

View File

@@ -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 \

View File

@@ -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."

View File

@@ -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}" _