refactor(test): share bundled channel Docker helpers

This commit is contained in:
Peter Steinberger
2026-04-29 06:42:09 +01:00
parent 5580d8951c
commit 83df409d94
7 changed files with 242 additions and 431 deletions

View File

@@ -21,6 +21,7 @@ run_channel_scenario() {
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"
@@ -79,154 +80,12 @@ test -d "$package_root/dist/extensions/slack"
test -d "$package_root/dist/extensions/feishu"
test -d "$package_root/dist/extensions/memory-lancedb"
stage_root() {
printf "%s/.openclaw/plugin-runtime-deps" "$HOME"
}
find_external_dep_package() {
local dep_path="$1"
find "$(stage_root)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
}
assert_package_dep_absent() {
local channel="$1"
local dep_path="$2"
for candidate in \
"$package_root/dist/extensions/$channel/node_modules/$dep_path/package.json" \
"$package_root/dist/extensions/node_modules/$dep_path/package.json" \
"$package_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 [ -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
write_config() {
local mode="$1"
node - <<'NODE' "$mode" "$TOKEN" "$PORT"
const fs = require("node:fs");
const path = require("node:path");
const mode = process.argv[2];
const token = process.argv[3];
const port = Number(process.argv[4]);
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath)
? JSON.parse(fs.readFileSync(configPath, "utf8"))
: {};
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,
};
if (mode === "telegram") {
config.channels = {
...(config.channels || {}),
telegram: {
...(config.channels?.telegram || {}),
enabled: true,
dmPolicy: "disabled",
groupPolicy: "disabled",
},
};
}
if (mode === "discord") {
config.channels = {
...(config.channels || {}),
discord: {
...(config.channels?.discord || {}),
enabled: true,
dmPolicy: "disabled",
groupPolicy: "disabled",
},
};
}
if (mode === "slack") {
config.channels = {
...(config.channels || {}),
slack: {
...(config.channels?.slack || {}),
enabled: true,
},
};
}
if (mode === "feishu") {
config.channels = {
...(config.channels || {}),
feishu: {
...(config.channels?.feishu || {}),
enabled: true,
},
};
}
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: "~/.openclaw/memory/lancedb-e2e",
autoCapture: false,
autoRecall: false,
},
},
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
NODE
}
start_gateway() {
local log_file="$1"
local skip_sidecars="${2:-0}"
@@ -347,12 +206,12 @@ assert_installed_once() {
if [ "$count" -eq 1 ]; then
return 0
fi
if [ "$count" -eq 0 ] && [ -n "$(find_external_dep_package "$dep_path")" ]; then
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 "$(stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
find "$(bundled_channel_stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
}
@@ -369,24 +228,13 @@ assert_not_installed() {
assert_dep_sentinel() {
local channel="$1"
local dep_path="$2"
local sentinel
sentinel="$(find_external_dep_package "$dep_path")"
if [ -z "$sentinel" ]; then
echo "missing external dependency sentinel for $channel: $dep_path" >&2
find "$(stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
fi
assert_package_dep_absent "$channel" "$dep_path"
bundled_channel_assert_dep_available "$channel" "$dep_path" "$package_root"
}
assert_no_dep_sentinel() {
local channel="$1"
local dep_path="$2"
assert_package_dep_absent "$channel" "$dep_path"
if [ -n "$(find_external_dep_package "$dep_path")" ]; then
echo "external dependency sentinel should be absent before activation for $channel: $dep_path" >&2
exit 1
fi
bundled_channel_assert_no_dep_available "$channel" "$dep_path" "$package_root"
}
assert_no_install_stage() {
@@ -400,14 +248,14 @@ assert_no_install_stage() {
}
echo "Starting baseline gateway with OpenAI configured..."
write_config baseline
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..."
write_config "$CHANNEL"
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"

View File

@@ -0,0 +1,194 @@
#!/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_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_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_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 - <<'NODE' "$mode" "${TOKEN:?missing TOKEN}" "${PORT:?missing PORT}"
const fs = require("node:fs");
const path = require("node:path");
const mode = process.argv[2];
const token = process.argv[3];
const port = Number(process.argv[4]);
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath)
? JSON.parse(fs.readFileSync(configPath, "utf8"))
: {};
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: "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: "xoxb-bundled-channel-update-token",
appToken: "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,
},
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
NODE
}

View File

@@ -17,6 +17,7 @@ run_disabled_config_scenario() {
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"
@@ -24,10 +25,6 @@ export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_PLUGIN_STAGE_DIR="$HOME/.openclaw/plugin-runtime-deps"
mkdir -p "$OPENCLAW_PLUGIN_STAGE_DIR"
package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
assert_dep_absent_everywhere() {
local channel="$1"
local dep_path="$2"
@@ -99,7 +96,7 @@ 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-disabled-config-install.log 2>&1
root="$(package_root)"
root="$(bundled_channel_package_root)"
test -d "$root/dist/extensions/telegram"
test -d "$root/dist/extensions/discord"
test -d "$root/dist/extensions/slack"

View File

@@ -17,20 +17,17 @@ run_load_failure_scenario() {
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
package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
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-load-failure-install.log 2>&1
root="$(package_root)"
root="$(bundled_channel_package_root)"
plugin_dir="$root/dist/extensions/load-failure-alpha"
mkdir -p "$plugin_dir"
cat >"$plugin_dir/package.json" <<'JSON'

View File

@@ -13,6 +13,7 @@ run_root_owned_global_scenario() {
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
@@ -24,10 +25,6 @@ CHANNEL="slack"
DEP_SENTINEL="@slack/web-api"
gateway_pid=""
package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
cleanup() {
if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then
kill "$gateway_pid" 2>/dev/null || true
@@ -44,7 +41,7 @@ if ! npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-root-owne
exit 1
fi
root="$(package_root)"
root="$(bundled_channel_package_root)"
test -d "$root/dist/extensions/$CHANNEL"
rm -rf "$root/dist/extensions/$CHANNEL/node_modules"
chmod -R a-w "$root"

View File

@@ -17,6 +17,7 @@ run_setup_entry_scenario() {
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"
@@ -29,10 +30,6 @@ declare -A SETUP_ENTRY_DEP_SENTINELS=(
[whatsapp]="@whiskeysockets/baileys"
)
package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
echo "Installing mounted OpenClaw package..."
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
if ! npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-setup-entry-install.log 2>&1; then
@@ -40,7 +37,7 @@ if ! npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-setup-ent
exit 1
fi
root="$(package_root)"
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"

View File

@@ -19,25 +19,19 @@ run_update_scenario() {
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}"
package_root() {
printf "%s/openclaw" "$(npm root -g)"
}
stage_root() {
printf "%s/.openclaw/plugin-runtime-deps" "$HOME"
}
poison_home_npm_project() {
printf '{"name":"openclaw-home-prefix-poison","private":true}\n' >"$HOME/package.json"
rm -rf "$HOME/node_modules"
@@ -45,15 +39,10 @@ poison_home_npm_project() {
chmod 500 "$HOME/node_modules"
}
find_external_dep_package() {
local dep_path="$1"
find "$(stage_root)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
}
assert_no_unknown_stage_roots() {
if find "$(stage_root)" -maxdepth 1 -type d -name 'openclaw-unknown-*' -print -quit 2>/dev/null | grep -q .; then
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 "$(stage_root)" -maxdepth 1 -type d -name 'openclaw-*' -print | sort >&2 || true
find "$(bundled_channel_stage_root)" -maxdepth 1 -type d -name 'openclaw-*' -print | sort >&2 || true
exit 1
fi
}
@@ -69,214 +58,6 @@ process.stdout.write(String(JSON.parse(raw).version));
NODE
)"
write_config() {
local mode="$1"
node - <<'NODE' "$mode" "$TOKEN" "$PORT"
const fs = require("node:fs");
const path = require("node:path");
const mode = process.argv[2];
const token = process.argv[3];
const port = Number(process.argv[4]);
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath)
? JSON.parse(fs.readFileSync(configPath, "utf8"))
: {};
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: "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: "xoxb-bundled-channel-update-token",
appToken: "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: "~/.openclaw/memory/lancedb-update-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,
},
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
NODE
}
assert_dep_sentinel() {
local channel="$1"
local dep_path="$2"
local root
local sentinel
root="$(package_root)"
sentinel="$(find_external_dep_package "$dep_path")"
if [ -z "$sentinel" ]; then
echo "missing external dependency sentinel for $channel: $dep_path" >&2
find "$(stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
fi
assert_no_package_dep_available "$channel" "$dep_path" "$root"
}
assert_no_dep_sentinel() {
local channel="$1"
local dep_path="$2"
local root
root="$(package_root)"
assert_no_package_dep_available "$channel" "$dep_path" "$root"
if [ -n "$(find_external_dep_package "$dep_path")" ]; then
echo "external dependency sentinel should be absent before repair for $channel: $dep_path" >&2
exit 1
fi
}
assert_no_package_dep_available() {
local channel="$1"
local dep_path="$2"
local root="$3"
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
}
assert_dep_available() {
local channel="$1"
local dep_path="$2"
local root
local sentinel
root="$(package_root)"
sentinel="$(find_external_dep_package "$dep_path")"
if [ -n "$sentinel" ]; then
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 "$(stage_root)" -maxdepth 12 -type f | sort | head -120 >&2 || true
exit 1
}
assert_no_dep_available() {
local channel="$1"
local dep_path="$2"
local root
root="$(package_root)"
assert_no_package_dep_available "$channel" "$dep_path" "$root"
if [ -n "$(find_external_dep_package "$dep_path")" ]; then
echo "dependency sentinel should be absent before repair for $channel: $dep_path" >&2
exit 1
fi
}
remove_runtime_dep() {
local channel="$1"
local dep_path="$2"
local root
root="$(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 "$(stage_root)"
}
assert_update_ok() {
local json_file="$1"
local expected_before="$2"
@@ -337,95 +118,95 @@ echo "Update targets: $UPDATE_TARGETS"
npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-update-baseline-install.log 2>&1
command -v openclaw >/dev/null
poison_home_npm_project
baseline_root="$(package_root)"
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..."
write_config telegram
assert_no_dep_available telegram grammy
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"
remove_runtime_dep telegram grammy
assert_no_dep_available telegram grammy
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"
assert_dep_available telegram grammy
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..."
remove_runtime_dep telegram grammy
assert_no_dep_available telegram grammy
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
assert_dep_available telegram grammy
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..."
write_config discord
remove_runtime_dep discord discord-api-types
assert_no_dep_available discord discord-api-types
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"
assert_dep_available discord discord-api-types
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..."
write_config slack
remove_runtime_dep slack @slack/web-api
assert_no_dep_available slack @slack/web-api
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"
assert_dep_available slack @slack/web-api
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..."
write_config feishu
remove_runtime_dep feishu @larksuiteoapi/node-sdk
assert_no_dep_available feishu @larksuiteoapi/node-sdk
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"
assert_dep_available feishu @larksuiteoapi/node-sdk
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..."
write_config memory-lancedb
remove_runtime_dep memory-lancedb @lancedb/lancedb
assert_no_dep_available memory-lancedb @lancedb/lancedb
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"
assert_dep_available memory-lancedb @lancedb/lancedb
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..."
write_config acpx
remove_runtime_dep acpx acpx
assert_no_dep_available acpx acpx
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"
assert_dep_available acpx acpx
bundled_channel_assert_dep_available acpx acpx
fi
echo "bundled channel runtime deps Docker update E2E passed"