mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:50:43 +00:00
442 lines
14 KiB
Bash
442 lines
14 KiB
Bash
#!/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() {
|
|
local run_log
|
|
local state_script_b64
|
|
run_log="$(docker_e2e_run_log bundled-channel-update)"
|
|
state_script_b64="$(docker_e2e_test_state_shell_b64 bundled-channel-update empty)"
|
|
|
|
echo "Running bundled channel runtime deps Docker update E2E..."
|
|
if ! timeout "$DOCKER_RUN_TIMEOUT" docker run --rm \
|
|
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
|
-e OPENCLAW_BUNDLED_CHANNEL_UPDATE_BASELINE_VERSION="$UPDATE_BASELINE_VERSION" \
|
|
-e "OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=${OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS:-telegram,discord,slack,feishu,memory-lancedb,acpx}" \
|
|
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
|
|
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
|
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
|
|
set -euo pipefail
|
|
|
|
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
|
|
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=""
|
|
|
|
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"
|
|
mkdir -p "$HOME/node_modules"
|
|
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
|
|
echo "runtime deps created second-generation unknown stage roots" >&2
|
|
find "$(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 - <<'NODE' "$package_tgz"
|
|
const { execFileSync } = require("node:child_process");
|
|
const raw = execFileSync("tar", ["-xOf", process.argv[2], "package/package.json"], {
|
|
encoding: "utf8",
|
|
});
|
|
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"
|
|
node - <<'NODE' "$json_file" "$expected_before" "$candidate_version"
|
|
const fs = require("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)}`);
|
|
}
|
|
NODE
|
|
}
|
|
|
|
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 "Installing current candidate as update baseline..."
|
|
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)"
|
|
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
|
|
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
|
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
fi
|
|
|
|
echo "bundled channel runtime deps Docker update E2E passed"
|
|
EOF
|
|
then
|
|
docker_e2e_print_log "$run_log"
|
|
rm -f "$run_log"
|
|
exit 1
|
|
fi
|
|
|
|
docker_e2e_print_log "$run_log"
|
|
rm -f "$run_log"
|
|
}
|