refactor: simplify plugin dependency handling

Simplify plugin installation and runtime loading around package-manager-owned dependencies, with Jiti reserved for local/TS fallback paths.

Also scans npm plugin install roots so hoisted transitive dependencies are covered by dependency denylist and node_modules symlink checks.
This commit is contained in:
Peter Steinberger
2026-05-01 21:32:22 +01:00
committed by GitHub
parent 2e8e9cd6ca
commit ed8f50f240
294 changed files with 2562 additions and 25454 deletions

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Runs bundled plugin runtime-dependency Docker scenarios from a mounted OpenClaw
# npm tarball. The default image is a clean runner; each scenario installs the
# tarball so package install behavior is what gets tested.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel-runtime-deps-runner.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/channel.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/root-owned.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/setup-entry.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/disabled-config.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/update.sh"
source "$ROOT_DIR/scripts/e2e/lib/bundled-channel/load-failure.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-bundled-channel-deps-e2e" OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE)"
UPDATE_BASELINE_VERSION="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_BASELINE_VERSION:-2026.4.20}"
DOCKER_TARGET="${OPENCLAW_BUNDLED_CHANNEL_DOCKER_TARGET:-bare}"
HOST_BUILD="${OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD:-1}"
PACKAGE_TGZ="${OPENCLAW_CURRENT_PACKAGE_TGZ:-}"
RUN_CHANNEL_SCENARIOS="${OPENCLAW_BUNDLED_CHANNEL_SCENARIOS:-1}"
RUN_UPDATE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO:-1}"
RUN_ROOT_OWNED_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO:-1}"
RUN_SETUP_ENTRY_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO:-1}"
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"
prepare_package_tgz() {
if [ -n "$PACKAGE_TGZ" ]; then
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz bundled-channel-deps "$PACKAGE_TGZ")"
return 0
fi
if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then
echo "OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz bundled-channel-deps)"
}
prepare_package_tgz
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_harness_mount_args
run_bundled_channel_runtime_dep_scenarios

View File

@@ -1,83 +0,0 @@
#!/usr/bin/env bash
#
# Scenario selection for bundled plugin runtime-dependency Docker tests.
# The large scenario bodies stay in the owning test script; this helper keeps
# env flag parsing and dispatch in one small, reviewable place.
bundled_channel_state_script_b64() {
docker_e2e_test_state_shell_b64 "$1" empty
}
run_bundled_channel_container() {
local label="$1"
local timeout_value="$2"
shift 2
run_logged_print "$label" timeout "$timeout_value" docker run --rm \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$@"
}
run_bundled_channel_container_with_state() {
local label="$1"
local timeout_value="$2"
local state_label="$3"
shift 3
local state_script_b64
state_script_b64="$(bundled_channel_state_script_b64 "$state_label")"
run_bundled_channel_container "$label" "$timeout_value" \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"$@"
}
run_bundled_channel_container_with_state_heartbeat() {
local label="$1"
local heartbeat="$2"
local timeout_value="$3"
local state_label="$4"
shift 4
local state_script_b64
state_script_b64="$(bundled_channel_state_script_b64 "$state_label")"
run_logged_print_heartbeat "$label" "$heartbeat" timeout "$timeout_value" docker run --rm \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
"$@"
}
run_bundled_channel_runtime_dep_scenarios() {
if [ "$RUN_CHANNEL_SCENARIOS" != "0" ]; then
IFS=',' read -r -a CHANNEL_SCENARIOS <<<"${OPENCLAW_BUNDLED_CHANNELS:-${CHANNEL_ONLY:-telegram,discord,slack,feishu,memory-lancedb}}"
for channel_scenario in "${CHANNEL_SCENARIOS[@]}"; do
channel_scenario="${channel_scenario//[[:space:]]/}"
[ -n "$channel_scenario" ] || continue
case "$channel_scenario" in
telegram) run_channel_scenario telegram grammy ;;
discord) run_channel_scenario discord discord-api-types ;;
slack) run_channel_scenario slack @slack/web-api ;;
feishu) run_channel_scenario feishu @larksuiteoapi/node-sdk ;;
memory-lancedb) run_channel_scenario memory-lancedb @lancedb/lancedb ;;
*)
echo "Unsupported OPENCLAW_BUNDLED_CHANNELS entry: $channel_scenario" >&2
exit 1
;;
esac
done
fi
if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then
run_update_scenario
fi
if [ "$RUN_ROOT_OWNED_SCENARIO" != "0" ]; then
run_root_owned_global_scenario
fi
if [ "$RUN_SETUP_ENTRY_SCENARIO" != "0" ]; then
run_setup_entry_scenario
fi
if [ "$RUN_DISABLED_CONFIG_SCENARIO" != "0" ]; then
run_disabled_config_scenario
fi
if [ "$RUN_LOAD_FAILURE_SCENARIO" != "0" ]; then
run_load_failure_scenario
fi
}

View File

@@ -1,22 +0,0 @@
import fs from "node:fs";
const raw = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const payload = raw.result ?? raw.data ?? raw;
const channel = process.argv[3];
const dump = () => JSON.stringify(raw, null, 2).slice(0, 4000);
const hasChannelMeta = Array.isArray(payload.channelMeta)
? payload.channelMeta.some((entry) => entry?.id === channel)
: Boolean(payload.channelMeta?.[channel]);
if (!hasChannelMeta) {
throw new Error(`missing channelMeta.${channel}\n${dump()}`);
}
if (!payload.channels || !payload.channels[channel]) {
throw new Error(`missing channels.${channel}\n${dump()}`);
}
const accounts = payload.channelAccounts?.[channel];
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error(`missing channelAccounts.${channel}\n${dump()}`);
}
console.log(`${channel} channel plugin visible`);

View File

@@ -1,44 +0,0 @@
import fs from "node:fs";
import path from "node:path";
const stageDir = process.argv[2];
const depName = process.argv[3];
const manifestName = ".openclaw-runtime-deps.json";
const matches = [];
function visit(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
continue;
}
if (entry.name !== manifestName) {
continue;
}
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
} catch {
continue;
}
const specs = Array.isArray(parsed.specs) ? parsed.specs : [];
for (const spec of specs) {
if (typeof spec === "string" && spec.startsWith(`${depName}@`)) {
matches.push(`${fullPath}: ${spec}`);
}
}
}
}
visit(stageDir);
if (matches.length > 0) {
process.stderr.write(`${matches.join("\n")}\n`);
process.exit(1);
}

View File

@@ -1,26 +0,0 @@
import fs from "node:fs";
const payload = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const expectedBefore = process.argv[3];
const expectedAfter = process.argv[4];
if (payload.status !== "ok") {
throw new Error(`expected update status ok, got ${JSON.stringify(payload.status)}`);
}
if (expectedBefore && (payload.before?.version ?? null) !== expectedBefore) {
throw new Error(
`expected before.version ${expectedBefore}, got ${JSON.stringify(payload.before?.version)}`,
);
}
if ((payload.after?.version ?? null) !== expectedAfter) {
throw new Error(
`expected after.version ${expectedAfter}, got ${JSON.stringify(payload.after?.version)}`,
);
}
const steps = Array.isArray(payload.steps) ? payload.steps : [];
const doctor = steps.find((step) => step?.name === "openclaw doctor");
if (!doctor) {
throw new Error("missing openclaw doctor step");
}
if (Number(doctor.exitCode ?? 1) !== 0) {
throw new Error(`openclaw doctor step failed: ${JSON.stringify(doctor)}`);
}

View File

@@ -1,224 +0,0 @@
#!/usr/bin/env bash
#
# Runs one bundled plugin channel runtime-dependency scenario.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_channel_scenario() {
local channel="$1"
local dep_sentinel="$2"
echo "Running bundled $channel runtime deps Docker E2E..."
run_bundled_channel_container_with_state \
"bundled-channel-deps-$channel" \
"$DOCKER_RUN_TIMEOUT" \
"bundled-channel-deps-$channel" \
-e OPENCLAW_CHANNEL_UNDER_TEST="$channel" \
-e OPENCLAW_DEP_SENTINEL="$dep_sentinel" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.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"
export OPENCLAW_NO_ONBOARD=1
TOKEN="bundled-channel-deps-token"
PORT="18789"
CHANNEL="${OPENCLAW_CHANNEL_UNDER_TEST:?missing OPENCLAW_CHANNEL_UNDER_TEST}"
DEP_SENTINEL="${OPENCLAW_DEP_SENTINEL:?missing OPENCLAW_DEP_SENTINEL}"
gateway_pid=""
terminate_gateways() {
openclaw_e2e_terminate_gateways "${gateway_pid:-}"
}
cleanup() {
terminate_gateways
}
trap cleanup EXIT
bundled_channel_install_package /tmp/openclaw-install.log
command -v openclaw >/dev/null
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
find "$package_root/dist/extensions/$CHANNEL/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
start_gateway() {
local log_file="$1"
local skip_sidecars="${2:-0}"
: >"$log_file"
if [ "$skip_sidecars" = "1" ]; then
OPENCLAW_SKIP_CHANNELS=1 OPENCLAW_SKIP_PROVIDERS=1 \
openclaw gateway --port "$PORT" --bind loopback --allow-unconfigured >"$log_file" 2>&1 &
else
openclaw gateway --port "$PORT" --bind loopback --allow-unconfigured >"$log_file" 2>&1 &
fi
gateway_pid="$!"
# Cold bundled dependency staging can exceed 60s under 10-way Docker aggregate load.
for _ in $(seq 1 1200); do
if grep -Eq "listening on ws://|\\[gateway\\] http server listening|\\[gateway\\] ready( \\(|$)" "$log_file"; then
return 0
fi
if ! kill -0 "$gateway_pid" 2>/dev/null; then
echo "gateway exited unexpectedly" >&2
cat "$log_file" >&2
exit 1
fi
sleep 0.25
done
echo "timed out waiting for gateway" >&2
cat "$log_file" >&2
exit 1
}
stop_gateway() {
terminate_gateways
gateway_pid=""
}
wait_for_gateway_health() {
local log_file="${1:-}"
if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then
return 0
fi
echo "gateway process exited after ready marker" >&2
if [ -n "$log_file" ]; then
cat "$log_file" >&2
fi
return 1
}
parse_channel_status_json() {
local out="$1"
local channel="$2"
node scripts/e2e/lib/bundled-channel/assert-channel-status.mjs "$out" "$channel"
}
assert_channel_status() {
local channel="$1"
if [ "$channel" = "memory-lancedb" ]; then
echo "memory-lancedb plugin activation verified by dependency sentinel"
return 0
fi
local out="/tmp/openclaw-channel-status-$channel.json"
local err="/tmp/openclaw-channel-status-$channel.err"
local parse_err="/tmp/openclaw-channel-status-$channel.parse.err"
local parse_out="/tmp/openclaw-channel-status-$channel.parse.out"
for _ in $(seq 1 30); do
if openclaw gateway call channels.status \
--url "ws://127.0.0.1:$PORT" \
--token "$TOKEN" \
--timeout 10000 \
--json \
--params '{"probe":false}' >"$out" 2>"$err"; then
if parse_channel_status_json "$out" "$channel" >"$parse_out" 2>"$parse_err"; then
cat "$parse_out"
return 0
fi
fi
if grep -Eq "\\[gateway\\] ready \\(.*\\b$channel\\b" /tmp/openclaw-"$channel"-*.log 2>/dev/null; then
echo "$channel channel plugin visible in gateway ready log"
return 0
fi
sleep 2
done
if [ ! -s "$out" ]; then
cat "$err" >&2 || true
else
cat "$parse_err" >&2 || true
cat "$out" >&2 || true
fi
cat /tmp/openclaw-"$channel"-*.log >&2 2>/dev/null || true
return 1
}
assert_installed_once() {
local log_file="$1"
local channel="$2"
local dep_path="$3"
local count
count="$(grep -Ec "\\[plugins\\] $channel installed bundled runtime deps( in [0-9]+ms)?:" "$log_file" || true)"
if [ "$count" -eq 1 ]; then
return 0
fi
if [ "$count" -eq 0 ] && [ -n "$(bundled_channel_find_external_dep_package "$dep_path")" ]; then
return 0
fi
echo "expected one runtime deps install log or staged dependency sentinel for $channel, got $count log lines" >&2
cat "$log_file" >&2
find "$(bundled_channel_stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
}
assert_not_installed() {
local log_file="$1"
local channel="$2"
if grep -Eq "\\[plugins\\] $channel installed bundled runtime deps( in [0-9]+ms)?:" "$log_file"; then
echo "expected no runtime deps reinstall for $channel" >&2
cat "$log_file" >&2
exit 1
fi
}
assert_dep_sentinel() {
local channel="$1"
local dep_path="$2"
bundled_channel_assert_dep_available "$channel" "$dep_path" "$package_root"
}
assert_no_dep_sentinel() {
local channel="$1"
local dep_path="$2"
bundled_channel_assert_no_dep_available "$channel" "$dep_path" "$package_root"
}
assert_no_install_stage() {
local channel="$1"
local stage="$package_root/dist/extensions/$channel/.openclaw-install-stage"
if [ -e "$stage" ]; then
echo "install stage should be cleaned after activation for $channel" >&2
find "$stage" -maxdepth 4 -type f | sort | head -80 >&2 || true
exit 1
fi
}
echo "Starting baseline gateway with OpenAI configured..."
bundled_channel_write_config baseline
start_gateway "/tmp/openclaw-$CHANNEL-baseline.log" 1
wait_for_gateway_health "/tmp/openclaw-$CHANNEL-baseline.log"
stop_gateway
assert_no_dep_sentinel "$CHANNEL" "$DEP_SENTINEL"
echo "Enabling $CHANNEL by config edit, then restarting gateway..."
bundled_channel_write_config "$CHANNEL"
start_gateway "/tmp/openclaw-$CHANNEL-first.log"
wait_for_gateway_health "/tmp/openclaw-$CHANNEL-first.log"
assert_installed_once "/tmp/openclaw-$CHANNEL-first.log" "$CHANNEL" "$DEP_SENTINEL"
assert_dep_sentinel "$CHANNEL" "$DEP_SENTINEL"
assert_no_install_stage "$CHANNEL"
assert_channel_status "$CHANNEL"
stop_gateway
echo "Restarting gateway again; $CHANNEL deps must stay installed..."
start_gateway "/tmp/openclaw-$CHANNEL-second.log"
wait_for_gateway_health "/tmp/openclaw-$CHANNEL-second.log"
assert_not_installed "/tmp/openclaw-$CHANNEL-second.log" "$CHANNEL"
assert_no_install_stage "$CHANNEL"
assert_channel_status "$CHANNEL"
stop_gateway
echo "bundled $CHANNEL runtime deps Docker E2E passed"
EOF
}

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env bash
#
# Container-side helpers shared by bundled channel Docker E2E scenarios.
# These functions assume the OpenClaw package is installed globally inside the
# test container and the scenario has exported HOME/OPENAI_API_KEY as needed.
bundled_channel_package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
bundled_channel_stage_root() {
printf "%s/.openclaw/plugin-runtime-deps" "$HOME"
}
bundled_channel_stage_dir() {
printf "%s" "${OPENCLAW_PLUGIN_STAGE_DIR:-$(bundled_channel_stage_root)}"
}
bundled_channel_install_package() {
openclaw_e2e_install_package "$@"
}
bundled_channel_find_external_dep_package() {
local dep_path="$1"
find "$(bundled_channel_stage_root)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
}
bundled_channel_find_staged_dep_package() {
local dep_path="$1"
find "$(bundled_channel_stage_dir)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
}
bundled_channel_dump_stage_dir() {
find "$(bundled_channel_stage_dir)" -maxdepth 12 -type f | sort | head -160 >&2 || true
}
bundled_channel_assert_no_package_dep_available() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
for candidate in \
"$root/dist/extensions/$channel/node_modules/$dep_path/package.json" \
"$root/dist/extensions/node_modules/$dep_path/package.json" \
"$root/node_modules/$dep_path/package.json"; do
if [ -f "$candidate" ]; then
echo "packaged install should not mutate package tree for $channel: $candidate" >&2
exit 1
fi
done
if [ -f "$HOME/node_modules/$dep_path/package.json" ]; then
echo "bundled runtime deps should not use HOME npm project for $channel: $HOME/node_modules/$dep_path/package.json" >&2
exit 1
fi
}
bundled_channel_assert_dep_available() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
if [ -n "$(bundled_channel_find_external_dep_package "$dep_path")" ]; then
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
return 0
fi
echo "missing dependency sentinel for $channel: $dep_path" >&2
find "$root/dist/extensions/$channel" -maxdepth 3 -type f | sort | head -80 >&2 || true
find "$root/node_modules" -maxdepth 3 -path "*/$dep_path/package.json" -type f -print >&2 || true
find "$(bundled_channel_stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
}
bundled_channel_assert_no_dep_available() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
if [ -n "$(bundled_channel_find_external_dep_package "$dep_path")" ]; then
echo "dependency sentinel should be absent before repair for $channel: $dep_path" >&2
exit 1
fi
}
bundled_channel_assert_no_staged_dep() {
local channel="$1"
local dep_path="$2"
local message="${3:-$channel unexpectedly staged $dep_path}"
if [ -n "$(bundled_channel_find_staged_dep_package "$dep_path")" ]; then
echo "$message" >&2
bundled_channel_dump_stage_dir
exit 1
fi
}
bundled_channel_assert_staged_dep() {
local channel="$1"
local dep_path="$2"
local log_file="${3:-}"
if [ -n "$(bundled_channel_find_staged_dep_package "$dep_path")" ]; then
return 0
fi
echo "missing external staged dependency sentinel for $channel: $dep_path" >&2
if [ -n "$log_file" ]; then
cat "$log_file" >&2 || true
fi
bundled_channel_dump_stage_dir
exit 1
}
bundled_channel_assert_no_staged_manifest_spec() {
local channel="$1"
local dep_path="$2"
local log_file="${3:-}"
if ! node scripts/e2e/lib/bundled-channel/assert-no-staged-manifest-spec.mjs "$(bundled_channel_stage_dir)" "$dep_path"; then
echo "$channel unexpectedly selected $dep_path for external runtime deps" >&2
if [ -n "$log_file" ]; then
cat "$log_file" >&2 || true
fi
exit 1
fi
}
bundled_channel_remove_runtime_dep() {
local channel="$1"
local dep_path="$2"
local root="${3:-$(bundled_channel_package_root)}"
rm -rf "$root/dist/extensions/$channel/node_modules"
rm -rf "$root/dist/extensions/node_modules/$dep_path"
rm -rf "$root/node_modules/$dep_path"
rm -rf "$(bundled_channel_stage_root)"
}
bundled_channel_write_config() {
local mode="$1"
node scripts/e2e/lib/bundled-channel/write-config.mjs \
"$mode" \
"${TOKEN:-bundled-channel-config-token}" \
"${PORT:-18789}"
}

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env bash
#
# Runs disabled-config runtime-dependency isolation scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_disabled_config_scenario() {
echo "Running bundled channel disabled-config runtime deps Docker E2E..."
run_bundled_channel_container_with_state \
bundled-channel-disabled-config \
"$DOCKER_RUN_TIMEOUT" \
bundled-channel-disabled-config \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.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
export OPENCLAW_PLUGIN_STAGE_DIR="$HOME/.openclaw/plugin-runtime-deps"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR"
assert_dep_absent_everywhere() {
local channel="$1"
local dep_path="$2"
local root="$3"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
bundled_channel_assert_no_staged_manifest_spec "$channel" "$dep_path" /tmp/openclaw-disabled-config-doctor.log
}
bundled_channel_install_package /tmp/openclaw-disabled-config-install.log
root="$(bundled_channel_package_root)"
test -d "$root/dist/extensions/telegram"
test -d "$root/dist/extensions/discord"
test -d "$root/dist/extensions/slack"
rm -rf "$root/dist/extensions/telegram/node_modules"
rm -rf "$root/dist/extensions/discord/node_modules"
rm -rf "$root/dist/extensions/slack/node_modules"
bundled_channel_write_config disabled-config
if ! openclaw doctor --non-interactive >/tmp/openclaw-disabled-config-doctor.log 2>&1; then
echo "doctor failed for disabled-config runtime deps smoke" >&2
cat /tmp/openclaw-disabled-config-doctor.log >&2
exit 1
fi
assert_dep_absent_everywhere telegram grammy "$root"
assert_dep_absent_everywhere slack @slack/web-api "$root"
assert_dep_absent_everywhere discord discord-api-types "$root"
if grep -Eq "(grammy|@slack/web-api|discord-api-types)" /tmp/openclaw-disabled-config-doctor.log; then
echo "doctor installed runtime deps for an explicitly disabled channel/plugin" >&2
cat /tmp/openclaw-disabled-config-doctor.log >&2
exit 1
fi
echo "bundled channel disabled-config runtime deps Docker E2E passed"
EOF
}

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env node
import { readdir } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const root = process.argv[2] || process.env.OPENCLAW_PACKAGE_ROOT;
if (!root) {
throw new Error("missing package root");
}
const distDir = path.join(root, "dist");
const onboardChannelFiles = (await readdir(distDir))
.filter((entry) => /^onboard-channels-.*\.js$/.test(entry))
.toSorted();
let setupChannels;
for (const entry of onboardChannelFiles) {
const module = await import(pathToFileURL(path.join(distDir, entry)));
if (typeof module.setupChannels === "function") {
setupChannels = module.setupChannels;
break;
}
}
if (!setupChannels) {
throw new Error(
`could not find packaged setupChannels export in ${JSON.stringify(onboardChannelFiles)}`,
);
}
let channelSelectCount = 0;
const notes = [];
const prompter = {
intro: async () => {},
outro: async () => {},
note: async (body, title) => {
notes.push({ title, body });
},
confirm: async ({ message, initialValue }) => {
if (message === "Link WhatsApp now (QR)?") {
return false;
}
return initialValue ?? true;
},
select: async ({ message, options }) => {
if (message === "Select a channel") {
channelSelectCount += 1;
return channelSelectCount === 1 ? "whatsapp" : "__done__";
}
if (message === "Install WhatsApp plugin?") {
if (!options?.some((option) => option.value === "local")) {
throw new Error(`missing bundled local install option: ${JSON.stringify(options)}`);
}
return "local";
}
if (message === "WhatsApp phone setup") {
return "separate";
}
if (message === "WhatsApp DM policy") {
return "disabled";
}
throw new Error(`unexpected select prompt: ${message}`);
},
multiselect: async ({ message }) => {
throw new Error(`unexpected multiselect prompt: ${message}`);
},
text: async ({ message }) => {
throw new Error(`unexpected text prompt: ${message}`);
},
};
const runtime = {
log: (message) => console.log(message),
error: (message) => console.error(message),
};
const result = await setupChannels({ plugins: { enabled: true } }, runtime, prompter, {
deferStatusUntilSelection: true,
skipConfirm: true,
skipStatusNote: true,
skipDmPolicyPrompt: true,
initialSelection: ["whatsapp"],
});
if (!result.channels?.whatsapp) {
throw new Error(`WhatsApp setup did not write channel config: ${JSON.stringify(result)}`);
}
console.log("packaged guided WhatsApp setup completed");

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
#
# Runs load-failure isolation scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_load_failure_scenario() {
echo "Running bundled channel load-failure isolation Docker E2E..."
run_bundled_channel_container_with_state \
bundled-channel-load-failure \
"$DOCKER_RUN_TIMEOUT" \
bundled-channel-load-failure \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.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
bundled_channel_install_package /tmp/openclaw-load-failure-install.log
root="$(bundled_channel_package_root)"
plugin_dir="$root/dist/extensions/load-failure-alpha"
node scripts/e2e/lib/bundled-channel/write-load-failure-fixture.mjs "$plugin_dir"
echo "Loading synthetic failing bundled channel through packaged loader..."
node scripts/e2e/lib/bundled-channel/loader-probe.mjs load-failure "$root" load-failure-alpha
echo "bundled channel load-failure isolation Docker E2E passed"
EOF
}

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
function usage() {
console.error("Usage: loader-probe.mjs <setup-entries|load-failure> <package-root> [channel...]");
process.exit(2);
}
function findBundledLoader(root) {
const distDir = path.join(root, "dist");
const bundledPath = fs
.readdirSync(distDir)
.filter((entry) => /^bundled-[A-Za-z0-9_-]+\.js$/.test(entry))
.map((entry) => path.join(distDir, entry))
.find((entry) => fs.readFileSync(entry, "utf8").includes("src/channels/plugins/bundled.ts"));
if (!bundledPath) {
throw new Error("missing packaged bundled channel loader artifact");
}
return bundledPath;
}
function namedExport(module, name) {
const fn = Object.values(module).find(
(value) => typeof value === "function" && value.name === name,
);
if (typeof fn !== "function") {
throw new Error(
`missing packaged bundled loader export ${name}; exports=${Object.keys(module).join(",")}`,
);
}
return fn;
}
async function importBundled(root) {
return import(pathToFileURL(findBundledLoader(root)));
}
function loadCounts() {
return {
plugin: globalThis.__loadFailurePlugin,
setup: globalThis.__loadFailureSetup,
secrets: globalThis.__loadFailureSecrets,
setupSecrets: globalThis.__loadFailureSetupSecrets,
};
}
function exerciseLoaders(loaders, id) {
for (const [name, fn] of loaders) {
try {
fn(id);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("synthetic")) {
throw new Error(`bundled export ${name} leaked synthetic load failure: ${message}`, {
cause: error,
});
}
}
}
}
const [command, root, ...args] = process.argv.slice(2);
if (!command || !root) {
usage();
}
if (command === "load-failure") {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(root, "dist/extensions");
}
const bundled = await importBundled(root);
if (command === "setup-entries") {
const channels = args.length > 0 ? args : ["feishu", "whatsapp"];
const setupPluginLoader = namedExport(bundled, "getBundledChannelSetupPlugin");
for (const channel of channels) {
const plugin = setupPluginLoader(channel);
if (!plugin) {
throw new Error(`${channel} setup plugin did not load pre-config`);
}
if (plugin.id !== channel) {
throw new Error(`${channel} setup plugin id mismatch: ${plugin.id}`);
}
console.log(`${channel} setup plugin loaded pre-config`);
}
} else if (command === "load-failure") {
const id = args[0] || "load-failure-alpha";
const loaderNames = [
"getBundledChannelPlugin",
"getBundledChannelSetupPlugin",
"getBundledChannelSecrets",
"getBundledChannelSetupSecrets",
];
const loaders = loaderNames.map((name) => [name, namedExport(bundled, name)]);
exerciseLoaders(loaders, id);
const firstCounts = loadCounts();
exerciseLoaders(loaders, id);
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 (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");
} else {
usage();
}

View File

@@ -1,6 +0,0 @@
import { execFileSync } from "node:child_process";
const raw = execFileSync("tar", ["-xOf", process.argv[2], "package/package.json"], {
encoding: "utf8",
});
process.stdout.write(String(JSON.parse(raw).version));

View File

@@ -1,124 +0,0 @@
#!/usr/bin/env bash
#
# Runs the root-owned global install runtime-dependency scenario.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_root_owned_global_scenario() {
echo "Running bundled channel root-owned global install Docker E2E..."
run_bundled_channel_container bundled-channel-root-owned "$DOCKER_RUN_TIMEOUT" \
--user root \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.sh
export HOME="/root"
export OPENAI_API_KEY="sk-openclaw-bundled-channel-root-owned-e2e"
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_PLUGIN_STAGE_DIR="/var/lib/openclaw/plugin-runtime-deps"
TOKEN="bundled-channel-root-owned-token"
PORT="18791"
CHANNEL="slack"
DEP_SENTINEL="@slack/web-api"
gateway_pid=""
cleanup() {
if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then
kill "$gateway_pid" 2>/dev/null || true
wait "$gateway_pid" 2>/dev/null || true
fi
}
trap cleanup EXIT
bundled_channel_install_package /tmp/openclaw-root-owned-install.log "mounted OpenClaw package into root-owned global npm"
root="$(bundled_channel_package_root)"
test -d "$root/dist/extensions/$CHANNEL"
rm -rf "$root/dist/extensions/$CHANNEL/node_modules"
chmod -R a-w "$root"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR" /home/appuser/.openclaw
chown -R appuser:appuser /home/appuser/.openclaw /var/lib/openclaw
if runuser -u appuser -- test -w "$root"; then
echo "expected package root to be unwritable for appuser" >&2
exit 1
fi
OPENCLAW_BUNDLED_CHANNEL_CONFIG_PATH=/home/appuser/.openclaw/openclaw.json \
OPENCLAW_BUNDLED_CHANNEL_SLACK_BOT_TOKEN=xoxb-bundled-channel-root-owned-token \
OPENCLAW_BUNDLED_CHANNEL_SLACK_APP_TOKEN=xapp-bundled-channel-root-owned-token \
bundled_channel_write_config slack
chown appuser:appuser /home/appuser/.openclaw/openclaw.json
start_gateway() {
local log_file="$1"
: >"$log_file"
chown appuser:appuser "$log_file"
runuser -u appuser -- env \
HOME=/home/appuser \
OPENAI_API_KEY="$OPENAI_API_KEY" \
OPENCLAW_NO_ONBOARD=1 \
OPENCLAW_PLUGIN_STAGE_DIR="$OPENCLAW_PLUGIN_STAGE_DIR" \
npm_config_cache=/tmp/openclaw-root-owned-npm-cache \
bash -c 'openclaw gateway --port "$1" --bind loopback --allow-unconfigured >"$2" 2>&1' \
bash "$PORT" "$log_file" &
gateway_pid="$!"
# Cold bundled dependency staging can exceed 60s under 10-way Docker aggregate load.
for _ in $(seq 1 1200); do
if grep -Eq "listening on ws://|\\[gateway\\] http server listening|\\[gateway\\] ready( \\(|$)" "$log_file"; then
return 0
fi
if ! kill -0 "$gateway_pid" 2>/dev/null; then
echo "gateway exited unexpectedly" >&2
cat "$log_file" >&2
exit 1
fi
sleep 0.25
done
echo "timed out waiting for gateway" >&2
cat "$log_file" >&2
exit 1
}
wait_for_slack_provider_start() {
for _ in $(seq 1 180); do
if grep -Eq "\\[slack\\] \\[default\\] starting provider|An API error occurred: invalid_auth|\\[plugins\\] slack installed bundled runtime deps|\\[gateway\\] ready \\(.*\\bslack\\b" /tmp/openclaw-root-owned-gateway.log; then
return 0
fi
sleep 1
done
echo "timed out waiting for slack provider startup" >&2
cat /tmp/openclaw-root-owned-gateway.log >&2
exit 1
}
start_gateway /tmp/openclaw-root-owned-gateway.log
wait_for_slack_provider_start
bundled_channel_assert_no_package_dep_available "$CHANNEL" "$DEP_SENTINEL" "$root"
bundled_channel_assert_staged_dep "$CHANNEL" "$DEP_SENTINEL" /tmp/openclaw-root-owned-gateway.log
if [ -e "$root/dist/extensions/node_modules/openclaw/package.json" ]; then
echo "root-owned package tree was mutated with SDK alias" >&2
find "$root/dist/extensions/node_modules/openclaw" -maxdepth 4 -type f | sort | head -80 >&2 || true
exit 1
fi
if ! find "$(bundled_channel_stage_dir)" -maxdepth 12 -path "*/dist/extensions/node_modules/openclaw/package.json" -type f | grep -q .; then
echo "missing external staged openclaw/plugin-sdk alias" >&2
bundled_channel_dump_stage_dir
cat /tmp/openclaw-root-owned-gateway.log >&2
exit 1
fi
if grep -Eq "failed to install bundled runtime deps|Cannot find package 'openclaw'|Cannot find module 'openclaw/plugin-sdk'" /tmp/openclaw-root-owned-gateway.log; then
echo "root-owned gateway hit bundled runtime dependency errors" >&2
cat /tmp/openclaw-root-owned-gateway.log >&2
exit 1
fi
echo "root-owned global install Docker E2E passed"
EOF
}

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env bash
#
# Runs setup-entry runtime-dependency installation scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_setup_entry_scenario() {
echo "Running bundled channel setup-entry runtime deps Docker E2E..."
run_bundled_channel_container_with_state \
bundled-channel-setup-entry \
"$DOCKER_RUN_TIMEOUT" \
bundled-channel-setup-entry \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.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
export OPENCLAW_PLUGIN_STAGE_DIR="$HOME/.openclaw/plugin-runtime-deps"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR"
declare -A SETUP_ENTRY_DEP_SENTINELS=(
[feishu]="@larksuiteoapi/node-sdk"
[whatsapp]="@whiskeysockets/baileys"
)
bundled_channel_install_package /tmp/openclaw-setup-entry-install.log
root="$(bundled_channel_package_root)"
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
test -d "$root/dist/extensions/$channel"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
done
echo "Probing real bundled setup entries before channel configuration..."
node scripts/e2e/lib/bundled-channel/loader-probe.mjs setup-entries "$root" feishu whatsapp
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
bundled_channel_assert_no_staged_dep "$channel" "$dep_sentinel" "setup-entry discovery installed $channel external staged deps before channel configuration"
done
echo "Running packaged guided WhatsApp setup; runtime deps should be staged before finalize..."
node scripts/e2e/lib/bundled-channel/guided-whatsapp-setup.mjs "$root"
bundled_channel_assert_no_package_dep_available whatsapp @whiskeysockets/baileys "$root"
bundled_channel_assert_staged_dep whatsapp @whiskeysockets/baileys
echo "Configuring setup-entry channels; doctor should now install bundled runtime deps externally..."
bundled_channel_write_config setup-entry-channels
openclaw doctor --fix --non-interactive >/tmp/openclaw-setup-entry-doctor.log 2>&1
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
bundled_channel_assert_staged_dep "$channel" "$dep_sentinel" /tmp/openclaw-setup-entry-doctor.log
done
echo "bundled channel setup-entry runtime deps Docker E2E passed"
EOF
}

View File

@@ -1,178 +0,0 @@
#!/usr/bin/env bash
#
# Runs baseline-to-current bundled plugin update scenarios.
# Sourced by scripts/e2e/bundled-channel-runtime-deps-docker.sh.
run_update_scenario() {
echo "Running bundled channel runtime deps Docker update E2E..."
run_bundled_channel_container_with_state_heartbeat \
bundled-channel-update \
30 \
"$DOCKER_UPDATE_RUN_TIMEOUT" \
bundled-channel-update \
-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}" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
source scripts/lib/openclaw-e2e-instance.sh
source scripts/e2e/lib/bundled-channel/common.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"
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_UPDATE_PACKAGE_SPEC=""
export OPENCLAW_BUNDLED_CHANNEL_MEMORY_DB_PATH="~/.openclaw/memory/lancedb-update-e2e"
TOKEN="bundled-channel-update-token"
PORT="18790"
UPDATE_TARGETS="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS:-telegram,discord,slack,feishu,memory-lancedb,acpx}"
poison_home_npm_project() {
printf '{"name":"openclaw-home-prefix-poison","private":true}\n' >"$HOME/package.json"
rm -rf "$HOME/node_modules"
mkdir -p "$HOME/node_modules"
chmod 500 "$HOME/node_modules"
}
assert_no_unknown_stage_roots() {
if find "$(bundled_channel_stage_root)" -maxdepth 1 -type d -name 'openclaw-unknown-*' -print -quit 2>/dev/null | grep -q .; then
echo "runtime deps created second-generation unknown stage roots" >&2
find "$(bundled_channel_stage_root)" -maxdepth 1 -type d -name 'openclaw-*' -print | sort >&2 || true
exit 1
fi
}
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
update_target="file:$package_tgz"
candidate_version="$(node scripts/e2e/lib/bundled-channel/package-version-from-tgz.mjs "$package_tgz")"
assert_update_ok() {
local json_file="$1"
local expected_before="$2"
node scripts/e2e/lib/bundled-channel/assert-update-result.mjs "$json_file" "$expected_before" "$candidate_version"
}
run_update_and_capture() {
local label="$1"
local out_file="$2"
set +e
openclaw update --tag "$update_target" --yes --json >"$out_file" 2>"/tmp/openclaw-$label-update.stderr"
local status=$?
set -e
if [ "$status" -ne 0 ]; then
echo "openclaw update failed for $label with exit code $status" >&2
cat "$out_file" >&2 || true
cat "/tmp/openclaw-$label-update.stderr" >&2 || true
exit "$status"
fi
}
should_run_update_target() {
local target="$1"
case ",$UPDATE_TARGETS," in
*",all,"* | *",$target,"*) return 0 ;;
*) return 1 ;;
esac
}
echo "Update targets: $UPDATE_TARGETS"
bundled_channel_install_package /tmp/openclaw-update-baseline-install.log "current candidate as update baseline"
command -v openclaw >/dev/null
poison_home_npm_project
baseline_root="$(bundled_channel_package_root)"
test -d "$baseline_root/dist/extensions/telegram"
test -d "$baseline_root/dist/extensions/feishu"
test -d "$baseline_root/dist/extensions/acpx"
if should_run_update_target telegram; then
echo "Replicating configured Telegram missing-runtime state..."
bundled_channel_write_config telegram
bundled_channel_assert_no_dep_available telegram grammy
set +e
openclaw doctor --non-interactive >/tmp/openclaw-baseline-doctor.log 2>&1
baseline_doctor_status=$?
set -e
echo "baseline doctor exited with $baseline_doctor_status"
bundled_channel_remove_runtime_dep telegram grammy
bundled_channel_assert_no_dep_available telegram grammy
echo "Updating from baseline to current candidate; candidate doctor must repair Telegram deps..."
run_update_and_capture telegram /tmp/openclaw-update-telegram.json
cat /tmp/openclaw-update-telegram.json
assert_update_ok /tmp/openclaw-update-telegram.json "$candidate_version"
bundled_channel_assert_dep_available telegram grammy
assert_no_unknown_stage_roots
echo "Mutating installed package: remove Telegram deps, then update-mode doctor repairs them..."
bundled_channel_remove_runtime_dep telegram grammy
bundled_channel_assert_no_dep_available telegram grammy
if ! OPENCLAW_UPDATE_IN_PROGRESS=1 openclaw doctor --non-interactive >/tmp/openclaw-update-mode-doctor.log 2>&1; then
echo "update-mode doctor failed while repairing Telegram deps" >&2
cat /tmp/openclaw-update-mode-doctor.log >&2
exit 1
fi
bundled_channel_assert_dep_available telegram grammy
assert_no_unknown_stage_roots
fi
if should_run_update_target discord; then
echo "Mutating config to Discord and rerunning same-version update path..."
bundled_channel_write_config discord
bundled_channel_remove_runtime_dep discord discord-api-types
bundled_channel_assert_no_dep_available discord discord-api-types
run_update_and_capture discord /tmp/openclaw-update-discord.json
cat /tmp/openclaw-update-discord.json
assert_update_ok /tmp/openclaw-update-discord.json "$candidate_version"
bundled_channel_assert_dep_available discord discord-api-types
fi
if should_run_update_target slack; then
echo "Mutating config to Slack and rerunning same-version update path..."
bundled_channel_write_config slack
bundled_channel_remove_runtime_dep slack @slack/web-api
bundled_channel_assert_no_dep_available slack @slack/web-api
run_update_and_capture slack /tmp/openclaw-update-slack.json
cat /tmp/openclaw-update-slack.json
assert_update_ok /tmp/openclaw-update-slack.json "$candidate_version"
bundled_channel_assert_dep_available slack @slack/web-api
fi
if should_run_update_target feishu; then
echo "Mutating config to Feishu and rerunning same-version update path..."
bundled_channel_write_config feishu
bundled_channel_remove_runtime_dep feishu @larksuiteoapi/node-sdk
bundled_channel_assert_no_dep_available feishu @larksuiteoapi/node-sdk
run_update_and_capture feishu /tmp/openclaw-update-feishu.json
cat /tmp/openclaw-update-feishu.json
assert_update_ok /tmp/openclaw-update-feishu.json "$candidate_version"
bundled_channel_assert_dep_available feishu @larksuiteoapi/node-sdk
fi
if should_run_update_target memory-lancedb; then
echo "Mutating config to memory-lancedb and rerunning same-version update path..."
bundled_channel_write_config memory-lancedb
bundled_channel_remove_runtime_dep memory-lancedb @lancedb/lancedb
bundled_channel_assert_no_dep_available memory-lancedb @lancedb/lancedb
run_update_and_capture memory-lancedb /tmp/openclaw-update-memory-lancedb.json
cat /tmp/openclaw-update-memory-lancedb.json
assert_update_ok /tmp/openclaw-update-memory-lancedb.json "$candidate_version"
bundled_channel_assert_dep_available memory-lancedb @lancedb/lancedb
fi
if should_run_update_target acpx; then
echo "Removing ACPX runtime package and rerunning same-version update path..."
bundled_channel_write_config acpx
bundled_channel_remove_runtime_dep acpx acpx
bundled_channel_assert_no_dep_available acpx acpx
run_update_and_capture acpx /tmp/openclaw-update-acpx.json
cat /tmp/openclaw-update-acpx.json
assert_update_ok /tmp/openclaw-update-acpx.json "$candidate_version"
bundled_channel_assert_dep_available acpx acpx
fi
echo "bundled channel runtime deps Docker update E2E passed"
EOF
}

View File

@@ -1,190 +0,0 @@
import fs from "node:fs";
import path from "node:path";
const mode = process.argv[2];
const token = process.argv[3];
const port = Number(process.argv[4]);
const configPath =
process.env.OPENCLAW_BUNDLED_CHANNEL_CONFIG_PATH ||
path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
if (mode === "disabled-config") {
const stateDir = path.dirname(configPath);
const disabledConfig = {
gateway: {
mode: "local",
auth: {
mode: "token",
token: "disabled-config-runtime-deps-token",
},
},
plugins: {
enabled: true,
entries: {
discord: { enabled: false },
},
},
channels: {
telegram: {
enabled: false,
botToken: "123456:disabled-config-token",
dmPolicy: "disabled",
groupPolicy: "disabled",
},
slack: {
enabled: false,
botToken: "xoxb-disabled-config-token",
appToken: "xapp-disabled-config-token",
},
discord: {
enabled: true,
token: "disabled-plugin-entry-token",
dmPolicy: "disabled",
groupPolicy: "disabled",
},
},
};
fs.mkdirSync(path.join(stateDir, "agents", "main", "sessions"), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(disabledConfig, null, 2)}\n`, "utf8");
fs.chmodSync(stateDir, 0o700);
fs.chmodSync(configPath, 0o600);
process.exit(0);
}
config.gateway = {
...config.gateway,
port,
auth: { mode: "token", token },
controlUi: { enabled: false },
};
config.agents = {
...config.agents,
defaults: {
...config.agents?.defaults,
model: { primary: "openai/gpt-4.1-mini" },
},
};
config.models = {
...config.models,
providers: {
...config.models?.providers,
openai: {
...config.models?.providers?.openai,
apiKey: process.env.OPENAI_API_KEY,
baseUrl: "https://api.openai.com/v1",
models: [],
},
},
};
config.plugins = {
...config.plugins,
enabled: true,
};
config.channels = {
...config.channels,
telegram: {
...config.channels?.telegram,
enabled: mode === "telegram",
botToken:
process.env.OPENCLAW_BUNDLED_CHANNEL_TELEGRAM_TOKEN || "123456:bundled-channel-update-token",
dmPolicy: "disabled",
groupPolicy: "disabled",
},
discord: {
...config.channels?.discord,
enabled: mode === "discord",
dmPolicy: "disabled",
groupPolicy: "disabled",
},
slack: {
...config.channels?.slack,
enabled: mode === "slack",
botToken:
process.env.OPENCLAW_BUNDLED_CHANNEL_SLACK_BOT_TOKEN || "xoxb-bundled-channel-update-token",
appToken:
process.env.OPENCLAW_BUNDLED_CHANNEL_SLACK_APP_TOKEN || "xapp-bundled-channel-update-token",
},
feishu: {
...config.channels?.feishu,
enabled: mode === "feishu",
},
};
if (mode === "memory-lancedb") {
config.plugins = {
...config.plugins,
enabled: true,
allow: [...new Set([...(config.plugins?.allow || []), "memory-lancedb"])],
slots: {
...config.plugins?.slots,
memory: "memory-lancedb",
},
entries: {
...config.plugins?.entries,
"memory-lancedb": {
...config.plugins?.entries?.["memory-lancedb"],
enabled: true,
config: {
...config.plugins?.entries?.["memory-lancedb"]?.config,
embedding: {
...config.plugins?.entries?.["memory-lancedb"]?.config?.embedding,
apiKey: process.env.OPENAI_API_KEY,
model: "text-embedding-3-small",
},
dbPath:
process.env.OPENCLAW_BUNDLED_CHANNEL_MEMORY_DB_PATH || "~/.openclaw/memory/lancedb-e2e",
autoCapture: false,
autoRecall: false,
},
},
},
};
}
if (mode === "acpx") {
config.plugins = {
...config.plugins,
enabled: true,
allow:
Array.isArray(config.plugins?.allow) && config.plugins.allow.length > 0
? [...new Set([...config.plugins.allow, "acpx"])]
: config.plugins?.allow,
entries: {
...config.plugins?.entries,
acpx: {
...config.plugins?.entries?.acpx,
enabled: true,
},
},
};
}
if (mode === "setup-entry-channels") {
config.plugins = {
...config.plugins,
enabled: true,
entries: {
...config.plugins?.entries,
feishu: {
...config.plugins?.entries?.feishu,
enabled: true,
},
whatsapp: {
...config.plugins?.entries?.whatsapp,
enabled: true,
},
},
};
config.channels = {
...config.channels,
feishu: {
...config.channels?.feishu,
enabled: true,
},
whatsapp: {
...config.channels?.whatsapp,
enabled: true,
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");

View File

@@ -1,42 +0,0 @@
import fs from "node:fs";
import path from "node:path";
const [pluginDir] = process.argv.slice(2);
if (!pluginDir) {
throw new Error("usage: write-load-failure-fixture.mjs <plugin-dir>");
}
const writeJson = (filename, contents) =>
fs.writeFileSync(path.join(pluginDir, filename), `${JSON.stringify(contents, null, 2)}\n`);
fs.mkdirSync(pluginDir, { recursive: true });
writeJson("package.json", {
name: "@openclaw/load-failure-alpha",
version: "2026.4.21",
private: true,
type: "module",
openclaw: { extensions: ["./index.js"], setupEntry: "./setup-entry.js" },
});
writeJson("openclaw.plugin.json", {
id: "load-failure-alpha",
channels: ["load-failure-alpha"],
configSchema: { type: "object", additionalProperties: false, properties: {} },
});
fs.writeFileSync(
path.join(pluginDir, "index.js"),
`export default {
kind: "bundled-channel-entry", id: "load-failure-alpha", name: "Load Failure Alpha", description: "Load Failure Alpha", register() {},
loadChannelSecrets() { globalThis.__loadFailureSecrets = (globalThis.__loadFailureSecrets ?? 0) + 1; throw new Error("synthetic channel secrets failure"); },
loadChannelPlugin() { globalThis.__loadFailurePlugin = (globalThis.__loadFailurePlugin ?? 0) + 1; throw new Error("synthetic channel plugin failure"); }
};
`,
);
fs.writeFileSync(
path.join(pluginDir, "setup-entry.js"),
`export default {
kind: "bundled-channel-setup-entry",
loadSetupSecrets() { globalThis.__loadFailureSetupSecrets = (globalThis.__loadFailureSetupSecrets ?? 0) + 1; throw new Error("synthetic setup secrets failure"); },
loadSetupPlugin() { globalThis.__loadFailureSetup = (globalThis.__loadFailureSetup ?? 0) + 1; throw new Error("synthetic setup plugin failure"); }
};
`,
);

View File

@@ -565,12 +565,7 @@ function findReadyLogIndex(logPath) {
function assertNoPostReadyRuntimeDepsWork(logPath, readyIndex) {
const log = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
const postReady = log.slice(Math.max(0, readyIndex));
const forbidden = [
/\[plugins\].*installed bundled runtime deps/iu,
/\[plugins\].*installing bundled runtime deps/iu,
/\[plugins\].*staging bundled runtime deps/iu,
/\b(?:npm|pnpm|yarn|corepack) install\b/iu,
];
const forbidden = [/\b(?:npm|pnpm|yarn|corepack) install\b/iu];
const match = forbidden.find((pattern) => pattern.test(postReady));
if (match) {
throw new Error(`post-ready runtime dependency work matched ${match}: ${tailText(postReady)}`);
@@ -578,14 +573,7 @@ function assertNoPostReadyRuntimeDepsWork(logPath, readyIndex) {
}
function assertNoRuntimeDepsLocks() {
const roots = [
...(process.env.OPENCLAW_PLUGIN_STAGE_DIR ? [process.env.OPENCLAW_PLUGIN_STAGE_DIR] : []),
path.join(
process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || os.homedir(), ".openclaw"),
"plugin-runtime-deps",
),
path.join(process.cwd(), "dist", "extensions"),
];
const roots = [path.join(process.cwd(), "dist", "extensions")];
for (const root of roots) {
if (!fs.existsSync(root)) {
continue;

View File

@@ -15,8 +15,6 @@ fi
export OPENCLAW_ENTRY
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
OPENCLAW_PLUGIN_STAGE_BASE_DIR="${OPENCLAW_PLUGIN_STAGE_DIR:-$HOME/.openclaw/plugin-runtime-deps}"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_BASE_DIR"
probe="scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs"
runtime_smoke="scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs"
@@ -33,8 +31,6 @@ echo "Selected ${#plugin_entries[@]} bundled plugins for shard ${OPENCLAW_BUNDLE
plugin_index=0
for plugin_entry in "${plugin_entries[@]}"; do
IFS=$'\t' read -r plugin_id plugin_dir requires_config <<<"$plugin_entry"
export OPENCLAW_PLUGIN_STAGE_DIR="$OPENCLAW_PLUGIN_STAGE_BASE_DIR/$plugin_index-$plugin_id"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR"
install_log="/tmp/openclaw-install-${plugin_index}.log"
uninstall_log="/tmp/openclaw-uninstall-${plugin_index}.log"
plugin_started_at="$(date +%s)"

View File

@@ -8,30 +8,46 @@ if (!url || !token) {
throw new Error("missing GW_URL/GW_TOKEN");
}
const CONNECT_READY_TIMEOUT_MS = Number.parseInt(
process.env.OPENCLAW_GATEWAY_NETWORK_CONNECT_READY_TIMEOUT_MS || "60000",
const deadlineMs = Number.parseInt(
process.env.OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS ??
process.env.OPENCLAW_GATEWAY_NETWORK_CONNECT_READY_TIMEOUT_MS ??
"80000",
10,
);
if (!Number.isFinite(deadlineMs) || deadlineMs < 0) {
throw new Error(`invalid gateway network client timeout: ${String(deadlineMs)}`);
}
const deadline = Date.now() + Math.max(1_000, deadlineMs);
async function openSocket() {
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function openSocket(timeoutMs = 10_000) {
const ws = new WebSocket(url);
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("ws open timeout")), 30_000);
const timer = setTimeout(() => {
ws.close();
reject(new Error("ws open timeout"));
}, timeoutMs);
ws.once("open", () => {
clearTimeout(timer);
resolve();
});
ws.once("error", (error) => {
clearTimeout(timer);
reject(error);
reject(error instanceof Error ? error : new Error(String(error)));
});
});
return ws;
}
function onceFrame(ws, filter, timeoutMs = 30_000) {
function onceFrame(ws, filter, timeoutMs = 10_000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
const timer = setTimeout(() => {
ws.off("message", handler);
reject(new Error("timeout"));
}, timeoutMs);
const handler = (data) => {
const obj = JSON.parse(String(data));
if (!filter(obj)) {
@@ -45,52 +61,51 @@ function onceFrame(ws, filter, timeoutMs = 30_000) {
});
}
async function attemptConnect() {
const ws = await openSocket();
ws.send(
JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
client: {
id: "test",
displayName: "docker-net-e2e",
version: "dev",
platform: process.platform,
mode: "test",
},
caps: [],
auth: { token },
},
}),
);
const connectRes = await onceFrame(ws, (frame) => frame?.type === "res" && frame?.id === "c1");
if (connectRes.ok) {
ws.close();
return;
}
ws.close();
throw new Error(`connect failed: ${connectRes.error?.message ?? "unknown"}`);
}
const startedAt = Date.now();
let lastError;
while (Date.now() - startedAt < CONNECT_READY_TIMEOUT_MS) {
while (Date.now() < deadline) {
let ws;
try {
await attemptConnect();
console.log("ok");
process.exit(0);
} catch (error) {
lastError = error;
if (!String(error).includes("gateway starting")) {
throw error;
ws = await openSocket();
ws.send(
JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
client: {
id: "test",
displayName: "docker-net-e2e",
version: "dev",
platform: process.platform,
mode: "test",
},
caps: [],
auth: { token },
},
}),
);
const connectRes = await onceFrame(ws, (frame) => frame?.type === "res" && frame?.id === "c1");
if (connectRes.ok) {
ws.close();
console.log("ok");
process.exit(0);
}
await new Promise((resolve) => setTimeout(resolve, 500));
const message = connectRes.error?.message ?? "unknown";
lastError = new Error(`connect failed: ${message}`);
if (!message.includes("gateway starting")) {
throw lastError;
}
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
} finally {
ws?.close();
}
await delay(500);
}
throw lastError ?? new Error("connect failed");
throw lastError ?? new Error("connect failed: timeout");

View File

@@ -144,7 +144,7 @@ const expectMissing = (listValue, expected, field) => {
}
};
const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "adversarial"]);
const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "conformance", "adversarial"]);
function assertExpectedDiagnostics(surfaceMode, errorMessages) {
const expectedErrorMessages = new Set([

View File

@@ -253,10 +253,11 @@ function assertGitPlugin() {
if (!installPath || !fs.existsSync(installPath)) {
throw new Error(`git install path missing on disk: ${installPath}`);
}
const extensionsRoot = path.join(process.env.HOME, ".openclaw", "extensions");
if (!installPath.startsWith(`${extensionsRoot}${path.sep}`)) {
throw new Error(`git install path is outside managed extensions root: ${installPath}`);
const gitRoot = path.join(process.env.HOME, ".openclaw", "git");
if (!installPath.endsWith(`${path.sep}repo`)) {
throw new Error(`git install path should point at cloned repo root: ${installPath}`);
}
assertRealPathInside(gitRoot, installPath, "git install path");
}
function assertRealPathInside(parentPath, childPath, label) {

View File

@@ -285,10 +285,19 @@ function assertStateSurvived() {
fs.existsSync(path.join(stateDir, "agents", "main", "sessions", "legacy-session.json")),
"legacy session file missing",
);
assert(
fs.existsSync(path.join(stateDir, "plugin-runtime-deps", "discord")),
"plugin runtime deps root missing",
);
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
const legacyRuntimeRoot = path.join(stateDir, "plugin-runtime-deps");
if (stage === "baseline") {
assert(
fs.existsSync(path.join(legacyRuntimeRoot, "discord")),
"legacy plugin runtime deps root missing before doctor cleanup",
);
} else {
assert(
!fs.existsSync(legacyRuntimeRoot),
`legacy plugin runtime deps root survived update/doctor: ${legacyRuntimeRoot}`,
);
}
if (scenario === "bootstrap-persona") {
for (const [fileName, contents] of PERSONA_FILES) {
const actual = fs.readFileSync(path.join(workspace, fileName), "utf8");
@@ -296,7 +305,6 @@ function assertStateSurvived() {
}
}
if (scenario === "versioned-runtime-deps") {
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
if (stage === "baseline") {
return;
}

View File

@@ -26,16 +26,31 @@ const baseUrl = option("--base-url");
const probePath = option("--path");
const expectKind = option("--expect");
const out = option("--out");
const url = new URL(probePath, baseUrl).toString();
const timeoutMs = Number.parseInt(
process.env.OPENCLAW_UPGRADE_SURVIVOR_PROBE_TIMEOUT_MS || "60000",
option("--timeout-ms", process.env.OPENCLAW_UPGRADE_SURVIVOR_PROBE_TIMEOUT_MS || "60000"),
10,
);
const url = new URL(probePath, baseUrl).toString();
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
throw new Error(`invalid --timeout-ms: ${String(timeoutMs)}`);
}
if (expectKind !== "live" && expectKind !== "ready") {
throw new Error(`unknown probe expectation: ${expectKind}`);
}
function matchesExpectation(body) {
if (expectKind === "live") {
return body?.ok === true && body?.status === "live";
}
return body?.ready === true;
}
const startedAt = Date.now();
let lastError;
while (Date.now() - startedAt < timeoutMs) {
const attemptStartedAt = Date.now();
let lastResult;
while (Date.now() - startedAt <= timeoutMs) {
try {
const response = await fetch(url, { method: "GET" });
const text = await response.text();
@@ -45,34 +60,31 @@ while (Date.now() - startedAt < timeoutMs) {
} catch (error) {
throw new Error(`${url} returned non-JSON probe body: ${String(error)}`, { cause: error });
}
if (!response.ok) {
throw new Error(`${url} probe failed with HTTP ${response.status}: ${text}`);
}
if (expectKind === "live") {
if (body?.ok !== true || body?.status !== "live") {
throw new Error(`${url} did not report live status: ${text}`);
}
} else if (expectKind === "ready") {
if (body?.ready !== true) {
throw new Error(`${url} did not report ready status: ${text}`);
}
} else {
throw new Error(`unknown probe expectation: ${expectKind}`);
}
writeJson(out, {
lastResult = {
body,
elapsedMs: Date.now() - startedAt,
path: probePath,
status: response.status,
url,
});
process.exit(0);
text,
};
if (response.ok && matchesExpectation(body)) {
writeJson(out, {
body,
elapsedMs: Date.now() - startedAt,
path: probePath,
status: response.status,
url,
});
process.exit(0);
}
lastError = response.ok
? `${url} did not report ${expectKind} status: ${text}`
: `${url} probe failed with HTTP ${response.status}: ${text}`;
} catch (error) {
lastError = error;
const elapsedMs = Date.now() - attemptStartedAt;
await new Promise((resolve) => setTimeout(resolve, Math.max(100, 500 - elapsedMs)));
lastError = error instanceof Error ? error.message : String(error);
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw lastError ?? new Error(`${url} probe timed out`);
const suffix = lastResult ? ` (last HTTP ${lastResult.status}: ${lastResult.text})` : "";
throw new Error(
`${url} probe did not satisfy ${expectKind} within ${timeoutMs}ms: ${lastError ?? "no response"}${suffix}`,
);

View File

@@ -302,7 +302,7 @@ assert_legacy_runtime_deps_symlink_repaired() {
local target_dir
target_dir="$(legacy_runtime_deps_symlink_target "$plugin")"
if [ -L "$target_dir" ]; then
echo "legacy runtime deps symlink survived package update: $target_dir -> $(readlink "$target_dir")" >&2
echo "legacy runtime deps symlink survived update/doctor: $target_dir -> $(readlink "$target_dir")" >&2
return 1
fi
echo "Legacy runtime deps symlink repaired for $plugin."
@@ -536,8 +536,8 @@ phase assert-baseline assert_baseline_state
phase seed-legacy-runtime-deps-symlink seed_legacy_runtime_deps_symlink
phase resolve-candidate resolve_candidate_version
phase update-candidate update_candidate
phase assert-legacy-runtime-deps-symlink-repaired assert_legacy_runtime_deps_symlink_repaired
phase doctor run_doctor
phase assert-legacy-runtime-deps-symlink-repaired assert_legacy_runtime_deps_symlink_repaired
phase validate-post-doctor-config validate_post_doctor_config
phase assert-survival assert_survival
phase gateway-start start_gateway

View File

@@ -134,7 +134,7 @@ node scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs assert-channel-con
echo "Running doctor after channel activation..."
openclaw doctor --repair --non-interactive >/tmp/openclaw-doctor.log 2>&1
openclaw_e2e_assert_dep_present "$DEP_SENTINEL" "$package_root" "$HOME/.openclaw"
openclaw_e2e_assert_dep_absent "$DEP_SENTINEL" "$package_root" "$HOME/.openclaw"
echo "Running local agent turn against mocked OpenAI..."
openclaw agent --local \

View File

@@ -304,12 +304,6 @@ if [ "${OPENCLAW_NPM_TELEGRAM_SKIP_HOTPATH:-0}" != "1" ]; then
openclaw channels add --channel telegram --token "123456:openclaw-npm-telegram-hotpath" >/tmp/openclaw-npm-telegram-channel-add.log 2>&1 </dev/null
openclaw doctor --fix --non-interactive >/tmp/openclaw-npm-telegram-doctor-fix.log 2>&1 </dev/null
openclaw doctor --non-interactive >/tmp/openclaw-npm-telegram-doctor-check.log 2>&1 </dev/null
if grep -F -q "Bundled plugin runtime deps are missing." /tmp/openclaw-npm-telegram-doctor-check.log; then
exit 1
fi
if grep -F -q "Failed to install bundled plugin runtime deps" /tmp/openclaw-npm-telegram-doctor-fix.log; then
exit 1
fi
fi
export OPENCLAW_NPM_TELEGRAM_SUT_COMMAND="$(command -v openclaw)"

View File

@@ -40,7 +40,7 @@ ${this.input.guestNode} ${this.input.guestOpenClawEntry} config set channels.dis
${this.input.guestNode} ${this.input.guestOpenClawEntry} config set channels.discord.enabled true
${this.input.guestNode} ${this.input.guestOpenClawEntry} config set channels.discord.groupPolicy allowlist
${this.input.guestNode} ${this.input.guestOpenClawEntry} config set channels.discord.guilds ${shellQuote(guilds)} --strict-json
${this.input.guestNode} ${this.input.guestOpenClawEntry} plugins deps --repair
${this.input.guestNode} ${this.input.guestOpenClawEntry} doctor --fix --yes --non-interactive
${this.input.guestNode} ${this.input.guestOpenClawEntry} gateway restart
${this.input.guestNode} ${this.input.guestOpenClawEntry} channels status --probe --json`);
}

View File

@@ -324,7 +324,6 @@ class MacosSmoke {
destination: this.tgzDir,
packageSpec: this.options.targetPackageSpec,
requireControlUi: true,
stageRuntimeDeps: !this.options.targetPackageSpec,
});
if (this.options.targetPackageSpec) {
this.targetExpectVersion =

View File

@@ -285,7 +285,6 @@ class NpmUpdateSmoke {
this.artifact = await packOpenClaw({
destination: this.tgzDir,
requireControlUi: true,
stageRuntimeDeps: true,
});
this.server = await startHostServer({
artifactPath: this.artifact.path,

View File

@@ -86,7 +86,6 @@ export async function packOpenClaw(input: {
destination: string;
packageSpec?: string;
requireControlUi?: boolean;
stageRuntimeDeps?: boolean;
}): Promise<PackageArtifact> {
await mkdir(input.destination, { recursive: true });
if (input.packageSpec) {
@@ -126,9 +125,6 @@ export async function packOpenClaw(input: {
"--eval",
"import { writePackageDistInventory } from './src/infra/package-dist-inventory.ts'; await writePackageDistInventory(process.cwd());",
]);
if (input.stageRuntimeDeps) {
run("node", ["scripts/stage-bundled-plugin-runtime-deps.mjs"]);
}
const shortHead = run("git", ["rev-parse", "--short", "HEAD"], { quiet: true }).stdout.trim();
const output = run(
"npm",