mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
refactor: simplify docker e2e harness scripts
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
157
scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs
Normal file
157
scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs
Normal 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)"}`);
|
||||
}
|
||||
51
scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh
Normal file
51
scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh
Normal 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))"
|
||||
29
scripts/e2e/lib/plugin-update/registry-server.mjs
Normal file
29
scripts/e2e/lib/plugin-update/registry-server.mjs
Normal 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");
|
||||
190
scripts/e2e/lib/plugin-update/unchanged-scenario.sh
Normal file
190
scripts/e2e/lib/plugin-update/unchanged-scenario.sh
Normal 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
|
||||
@@ -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 \
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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}" _
|
||||
|
||||
Reference in New Issue
Block a user