refactor(test): share bundled channel Docker harness helpers

This commit is contained in:
Peter Steinberger
2026-04-29 06:15:01 +01:00
parent f6d23ab5c2
commit 1476e24af3
9 changed files with 87 additions and 101 deletions

View File

@@ -627,7 +627,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the ClawHub block, or override the default kitchen-sink package/runtime pair with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`. Without `OPENCLAW_CLAWHUB_URL`/`CLAWHUB_URL`, the test uses a hermetic local ClawHub fixture server.
- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`)
- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`)
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate and release-path bundled-channel chunks pre-pack this tarball once, then shard bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. Release chunks split channel smokes, update targets, and setup/runtime contracts into `bundled-channels-core`, `bundled-channels-update-a`, `bundled-channels-update-b`, and `bundled-channels-contracts`; the aggregate `bundled-channels` chunk remains available for manual reruns. The release workflow also splits provider installer chunks and bundled plugin install/uninstall chunks; legacy `package-update`, `plugins-runtime`, and `plugins-integrations` chunks remain aggregate aliases for manual reruns. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate and release-path bundled-channel chunks pre-pack this tarball once, then shard bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. Release chunks split channel smokes, update targets, and setup/runtime contracts into `bundled-channels-core`, `bundled-channels-update-a`, `bundled-channels-update-b`, and `bundled-channels-contracts`; the aggregate `bundled-channels` chunk remains available for manual reruns. The release workflow also splits provider installer chunks and bundled plugin install/uninstall chunks; legacy `package-update`, `plugins-runtime`, and `plugins-integrations` chunks remain aggregate aliases for manual reruns. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. Per-scenario Docker runs default to `OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT=900s`; the multi-target update scenario defaults to `OPENCLAW_BUNDLED_CHANNEL_UPDATE_DOCKER_RUN_TIMEOUT=2400s`. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example:
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 pnpm test:docker:bundled-channel-deps`.

View File

@@ -28,6 +28,7 @@ RUN_LOAD_FAILURE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO:-1}"
RUN_DISABLED_CONFIG_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO:-1}"
CHANNEL_ONLY="${OPENCLAW_BUNDLED_CHANNEL_ONLY:-}"
DOCKER_RUN_TIMEOUT="${OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT:-900s}"
DOCKER_UPDATE_RUN_TIMEOUT="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_DOCKER_RUN_TIMEOUT:-${OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT:-2400s}}"
docker_e2e_build_or_reuse "$IMAGE_NAME" bundled-channel-deps "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET"
@@ -45,6 +46,7 @@ prepare_package_tgz() {
prepare_package_tgz
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_harness_mount_args
run_bundled_channel_runtime_dep_scenarios

View File

@@ -6,22 +6,22 @@
run_channel_scenario() {
local channel="$1"
local dep_sentinel="$2"
local run_log
local state_script_b64
run_log="$(docker_e2e_run_log "bundled-channel-deps-$channel")"
state_script_b64="$(docker_e2e_test_state_shell_b64 "bundled-channel-deps-$channel" empty)"
echo "Running bundled $channel runtime deps Docker E2E..."
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
run_logged_print "bundled-channel-deps-$channel" timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENCLAW_CHANNEL_UNDER_TEST="$channel" \
-e OPENCLAW_DEP_SENTINEL="$dep_sentinel" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
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}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENAI_API_KEY="sk-openclaw-bundled-channel-deps-e2e"
@@ -426,12 +426,4 @@ stop_gateway
echo "bundled $CHANNEL runtime deps Docker E2E passed"
EOF
then
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
docker_e2e_print_log "$run_log"
rm -f "$run_log"
}

View File

@@ -4,20 +4,20 @@
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_disabled_config_scenario() {
local run_log
local state_script_b64
run_log="$(docker_e2e_run_log bundled-channel-disabled-config)"
state_script_b64="$(docker_e2e_test_state_shell_b64 bundled-channel-disabled-config empty)"
echo "Running bundled channel disabled-config runtime deps Docker E2E..."
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
run_logged_print bundled-channel-disabled-config timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
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}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_NO_ONBOARD=1
@@ -171,12 +171,4 @@ fi
echo "bundled channel disabled-config runtime deps Docker E2E passed"
EOF
then
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
docker_e2e_print_log "$run_log"
rm -f "$run_log"
}

View File

@@ -4,20 +4,20 @@
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_load_failure_scenario() {
local run_log
local state_script_b64
run_log="$(docker_e2e_run_log bundled-channel-load-failure)"
state_script_b64="$(docker_e2e_test_state_shell_b64 bundled-channel-load-failure empty)"
echo "Running bundled channel load-failure isolation Docker E2E..."
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
run_logged_print bundled-channel-load-failure timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
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}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_NO_ONBOARD=1
@@ -106,16 +106,28 @@ if (!bundledPath) {
throw new Error("missing packaged bundled channel loader artifact");
}
const bundled = await import(pathToFileURL(bundledPath));
const oneArgExports = Object.entries(bundled).filter(
([, value]) => typeof value === "function" && value.length === 1,
const loaderNames = [
"getBundledChannelPlugin",
"getBundledChannelSetupPlugin",
"getBundledChannelSecrets",
"getBundledChannelSetupSecrets",
];
const exportedLoaders = new Map(
Object.values(bundled)
.filter((value) => typeof value === "function")
.map((fn) => [fn.name, fn]),
);
if (oneArgExports.length === 0) {
throw new Error(`missing one-argument bundled loader exports; exports=${Object.keys(bundled).join(",")}`);
}
const loaders = loaderNames.map((name) => {
const fn = exportedLoaders.get(name);
if (typeof fn !== "function") {
throw new Error(`missing packaged bundled loader export ${name}; exports=${Object.keys(bundled).join(",")}`);
}
return [name, fn];
});
const id = "load-failure-alpha";
for (let i = 0; i < 2; i += 1) {
for (const [name, fn] of oneArgExports) {
function exerciseLoaders() {
for (const [name, fn] of loaders) {
try {
fn(id);
} catch (error) {
@@ -127,23 +139,30 @@ for (let i = 0; i < 2; i += 1) {
}
}
const counts = {
plugin: globalThis.__loadFailurePlugin,
setup: globalThis.__loadFailureSetup,
secrets: globalThis.__loadFailureSecrets,
setupSecrets: globalThis.__loadFailureSetupSecrets,
};
for (const [key, value] of Object.entries({
plugin: counts.plugin,
setup: counts.setup,
setupSecrets: counts.setupSecrets,
})) {
if (value !== 1) {
throw new Error(`expected ${key} failure to be cached after one load, got ${value}`);
function loadCounts() {
return {
plugin: globalThis.__loadFailurePlugin,
setup: globalThis.__loadFailureSetup,
secrets: globalThis.__loadFailureSecrets,
setupSecrets: globalThis.__loadFailureSetupSecrets,
};
}
exerciseLoaders();
const firstCounts = loadCounts();
exerciseLoaders();
const secondCounts = loadCounts();
for (const key of ["plugin", "setup", "setupSecrets"]) {
const first = firstCounts[key];
if (!Number.isInteger(first) || first < 1) {
throw new Error(`expected ${key} failure to be exercised at least once, got ${first}`);
}
if (secondCounts[key] !== first) {
throw new Error(`expected ${key} failure to be cached after first pass, got ${first} then ${secondCounts[key]}`);
}
}
if (counts.secrets !== undefined && counts.secrets !== 1) {
throw new Error(`expected secrets failure to be cached after one load when exercised, got ${counts.secrets}`);
if (firstCounts.secrets !== undefined && secondCounts.secrets !== firstCounts.secrets) {
throw new Error(`expected secrets failure to be cached after first pass, got ${firstCounts.secrets} then ${secondCounts.secrets}`);
}
console.log("synthetic bundled channel load failures were isolated and cached");
NODE
@@ -151,12 +170,4 @@ NODE
echo "bundled channel load-failure isolation Docker E2E passed"
EOF
then
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
docker_e2e_print_log "$run_log"
rm -f "$run_log"
}

View File

@@ -4,16 +4,15 @@
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_root_owned_global_scenario() {
local run_log
run_log="$(docker_e2e_run_log bundled-channel-root-owned)"
echo "Running bundled channel root-owned global install Docker E2E..."
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm --user root \
run_logged_print bundled-channel-root-owned timeout "$DOCKER_RUN_TIMEOUT" docker run --rm --user root \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
export HOME="/root"
export OPENAI_API_KEY="sk-openclaw-bundled-channel-root-owned-e2e"
export OPENCLAW_NO_ONBOARD=1
@@ -174,12 +173,4 @@ fi
echo "root-owned global install Docker E2E passed"
EOF
then
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
docker_e2e_print_log "$run_log"
rm -f "$run_log"
}

View File

@@ -4,20 +4,20 @@
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_setup_entry_scenario() {
local run_log
local state_script_b64
run_log="$(docker_e2e_run_log bundled-channel-setup-entry)"
state_script_b64="$(docker_e2e_test_state_shell_b64 bundled-channel-setup-entry empty)"
echo "Running bundled channel setup-entry runtime deps Docker E2E..."
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
run_logged_print bundled-channel-setup-entry timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
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}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENCLAW_NO_ONBOARD=1
@@ -256,12 +256,4 @@ done
echo "bundled channel setup-entry runtime deps Docker E2E passed"
EOF
then
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
docker_e2e_print_log "$run_log"
rm -f "$run_log"
}

View File

@@ -4,22 +4,22 @@
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_update_scenario() {
local run_log
local state_script_b64
run_log="$(docker_e2e_run_log bundled-channel-update)"
state_script_b64="$(docker_e2e_test_state_shell_b64 bundled-channel-update empty)"
echo "Running bundled channel runtime deps Docker update E2E..."
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
run_logged_print bundled-channel-update timeout "$DOCKER_UPDATE_RUN_TIMEOUT" docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENCLAW_BUNDLED_CHANNEL_UPDATE_BASELINE_VERSION="$UPDATE_BASELINE_VERSION" \
-e "OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=${OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS:-telegram,discord,slack,feishu,memory-lancedb,acpx}" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
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}"
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
export OPENAI_API_KEY="sk-openclaw-bundled-channel-update-e2e"
@@ -430,12 +430,4 @@ fi
echo "bundled channel runtime deps Docker update E2E passed"
EOF
then
docker_e2e_print_log "$run_log"
rm -f "$run_log"
exit 1
fi
docker_e2e_print_log "$run_log"
rm -f "$run_log"
}

View File

@@ -17,6 +17,20 @@ run_logged() {
rm -f "$log_file"
}
run_logged_print() {
local label="$1"
shift
local log_file
log_file="$(docker_e2e_run_log "$label")"
if ! "$@" >"$log_file" 2>&1; then
docker_e2e_print_log "$log_file"
rm -f "$log_file"
return 1
fi
docker_e2e_print_log "$log_file"
rm -f "$log_file"
}
docker_e2e_run_log() {
local label="$1"
local tmp_dir="${TMPDIR:-/tmp}"