From d08f68dee733b04d5c20e472434c5bc7e2b20b19 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 13 May 2026 21:54:40 +0800 Subject: [PATCH] test(e2e): cover root-managed VPS upgrades --- .github/workflows/openclaw-release-checks.yml | 2 +- .github/workflows/package-acceptance.yml | 4 +- package.json | 1 + scripts/e2e/lib/upgrade-survivor/run.sh | 38 ++++++++++++++++++- scripts/e2e/upgrade-survivor-docker.sh | 14 +++++++ scripts/lib/docker-e2e-scenarios.mjs | 12 ++++++ test/scripts/docker-e2e-plan.test.ts | 11 ++++++ .../package-acceptance-workflow.test.ts | 4 +- 8 files changed, 79 insertions(+), 7 deletions(-) diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index 41997cc4fff..d20dd3cc173 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -626,7 +626,7 @@ jobs: artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} package_sha256: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.release_package_spec == '') && needs.prepare_release_package.outputs.package_sha256 || '' }} suite_profile: custom - docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update + docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }} published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }} telegram_mode: mock-openai diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index e4f3e4058be..d8be013d768 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -386,10 +386,10 @@ jobs: docker_lanes="npm-onboard-channel-agent gateway-network config-reload" ;; package) - docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update" + docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update" ;; product) - docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui" + docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui" include_openwebui=true ;; full) diff --git a/package.json b/package.json index f1d11a47372..36a1d9eaa31 100644 --- a/package.json +++ b/package.json @@ -1633,6 +1633,7 @@ "test:docker:published-upgrade-survivor": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest} OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT=${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s} bash scripts/e2e/upgrade-survivor-docker.sh", "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh", "test:docker:rerun": "node scripts/docker-e2e-rerun.mjs", + "test:docker:root-managed-vps-upgrade": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.5.7} OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT=${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s} bash scripts/e2e/upgrade-survivor-docker.sh", "test:docker:session-runtime-context": "bash scripts/e2e/session-runtime-context-docker.sh", "test:docker:skill-install": "bash scripts/e2e/skill-install-docker.sh", "test:docker:timings": "node scripts/docker-e2e-timings.mjs", diff --git a/scripts/e2e/lib/upgrade-survivor/run.sh b/scripts/e2e/lib/upgrade-survivor/run.sh index 9e3607671fc..ad2fa903c9b 100644 --- a/scripts/e2e/lib/upgrade-survivor/run.sh +++ b/scripts/e2e/lib/upgrade-survivor/run.sh @@ -38,6 +38,7 @@ CANDIDATE_KIND="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND:-tarball}" CANDIDATE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}" SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}" UPDATE_RESTART_MODE="${OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE:-manual}" +ROOT_MANAGED_VPS="${OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS:-0}" CURRENT_PHASE="setup" FAILURE_PHASE="" FAILURE_MESSAGE="" @@ -681,7 +682,16 @@ install_baseline() { seed_state() { openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_FUNCTION_B64:?missing OPENCLAW_TEST_STATE_FUNCTION_B64}" - openclaw_test_state_create "$ARTIFACT_ROOT/state-home" minimal + if [ "$ROOT_MANAGED_VPS" = "1" ]; then + if [ "$(id -u)" -ne 0 ]; then + echo "root-managed VPS survivor mode must run as uid 0" >&2 + return 1 + fi + rm -rf /root/.openclaw /root/workspace + openclaw_test_state_create /root minimal + else + openclaw_test_state_create "$ARTIFACT_ROOT/state-home" minimal + fi export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION="$baseline_version" node scripts/e2e/lib/upgrade-survivor/assertions.mjs seed } @@ -1037,12 +1047,21 @@ update_candidate() { local update_start="" local update_end="" local update_args=(update --tag "$CANDIDATE_SPEC" --yes --json) + local update_env=( + env + -u OPENCLAW_GATEWAY_TOKEN + -u OPENCLAW_GATEWAY_PASSWORD + -u OPENCLAW_ALLOW_ROOT + ) if [ "$UPDATE_RESTART_MODE" = "manual" ]; then update_args+=(--no-restart) else update_start="$(node -e "process.stdout.write(String(Date.now()))")" fi - if ! env -u OPENCLAW_GATEWAY_TOKEN -u OPENCLAW_GATEWAY_PASSWORD OPENCLAW_ALLOW_ROOT=1 openclaw "${update_args[@]}" >"$UPDATE_JSON" 2>"$UPDATE_ERR"; then + if [ "$ROOT_MANAGED_VPS" != "1" ]; then + update_env+=(OPENCLAW_ALLOW_ROOT=1) + fi + if ! "${update_env[@]}" openclaw "${update_args[@]}" >"$UPDATE_JSON" 2>"$UPDATE_ERR"; then echo "openclaw update failed" >&2 cat "$UPDATE_ERR" >&2 || true cat "$UPDATE_JSON" >&2 || true @@ -1063,6 +1082,20 @@ update_candidate() { installed_version="$(read_installed_version)" } +assert_root_managed_vps_cli_usable() { + if [ "$ROOT_MANAGED_VPS" != "1" ]; then + return 0 + fi + local root_cli_env=( + env + -u OPENCLAW_GATEWAY_TOKEN + -u OPENCLAW_GATEWAY_PASSWORD + -u OPENCLAW_ALLOW_ROOT + ) + "${root_cli_env[@]}" openclaw config file >"$ARTIFACT_ROOT/root-vps-config-file.out" 2>"$ARTIFACT_ROOT/root-vps-config-file.err" + "${root_cli_env[@]}" openclaw plugins >"$ARTIFACT_ROOT/root-vps-plugins.out" 2>"$ARTIFACT_ROOT/root-vps-plugins.err" +} + run_doctor() { if ! openclaw doctor --fix --non-interactive >"$DOCTOR_LOG" 2>&1; then echo "openclaw doctor failed" >&2 @@ -1183,6 +1216,7 @@ phase seed-legacy-runtime-deps-symlink seed_legacy_runtime_deps_symlink phase resolve-candidate resolve_candidate_version phase prepare-update-restart-probe prepare_update_restart_probe phase update-candidate update_candidate +phase root-managed-vps-cli-usable assert_root_managed_vps_cli_usable phase assert-legacy-plugin-dependency-debris-before-doctor assert_legacy_plugin_dependency_debris_before_doctor phase configure-configured-plugin-install-fixture-registry configure_configured_plugin_install_fixture_registry phase doctor run_doctor diff --git a/scripts/e2e/upgrade-survivor-docker.sh b/scripts/e2e/upgrade-survivor-docker.sh index 6ee195e5c02..6d5d9a244bf 100755 --- a/scripts/e2e/upgrade-survivor-docker.sh +++ b/scripts/e2e/upgrade-survivor-docker.sh @@ -17,6 +17,16 @@ UPDATE_RESTART_MODE="${OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE:-manual}" LANE_ARTIFACT_SUFFIX="${OPENCLAW_DOCKER_ALL_LANE_NAME:-default}" LANE_ARTIFACT_SUFFIX="${LANE_ARTIFACT_SUFFIX//[^A-Za-z0-9_.-]/_}" ARTIFACT_DIR="${OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR:-$ROOT_DIR/.artifacts/upgrade-survivor/$LANE_ARTIFACT_SUFFIX}" +ROOT_MANAGED_VPS="${OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS:-0}" +DOCKER_RUN_USER_ARGS=() + +if [ "$ROOT_MANAGED_VPS" = "1" ]; then + if [ "${OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE:-0}" != "1" ]; then + echo "OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS=1 requires OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1" >&2 + exit 1 + fi + DOCKER_RUN_USER_ARGS+=(--user root -e HOME=/root -e USER=root) +fi normalize_npm_candidate() { local raw="$1" @@ -89,11 +99,13 @@ if [ "${OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE:-0}" = "1" ]; then -e OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="$SCENARIO" \ -e OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE="$UPDATE_RESTART_MODE" \ -e OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK="${OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK:-}" \ + -e OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS="$ROOT_MANAGED_VPS" \ -e OPENCLAW_UPGRADE_SURVIVOR_SUMMARY_JSON=/tmp/openclaw-upgrade-survivor-artifacts/summary.json \ -e OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS="${OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS:-90}" \ -e OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS="${OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS:-30}" \ -v "$ARTIFACT_DIR:/tmp/openclaw-upgrade-survivor-artifacts" \ "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + "${DOCKER_RUN_USER_ARGS[@]}" \ "$IMAGE_NAME" \ timeout "$DOCKER_RUN_TIMEOUT" bash scripts/e2e/lib/upgrade-survivor/run.sh exit 0 @@ -112,12 +124,14 @@ docker_e2e_run_with_harness \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ -e OPENCLAW_TEST_STATE_SCRIPT_B64="$OPENCLAW_TEST_STATE_SCRIPT_B64" \ -e OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_ROOT=/tmp/openclaw-upgrade-survivor-artifacts \ + -e OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS="$ROOT_MANAGED_VPS" \ -e OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="$SCENARIO" \ -e OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE="$UPDATE_RESTART_MODE" \ -e OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS="${OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS:-90}" \ -e OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS="${OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS:-30}" \ -v "$ARTIFACT_DIR:/tmp/openclaw-upgrade-survivor-artifacts" \ "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + "${DOCKER_RUN_USER_ARGS[@]}" \ "$IMAGE_NAME" \ timeout "$DOCKER_RUN_TIMEOUT" bash -lc 'set -euo pipefail source scripts/lib/openclaw-e2e-instance.sh diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index 921f09ef8e2..b8be8ede54b 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -11,6 +11,8 @@ const RELEASE_OPENWEBUI_COMMAND = "OPENCLAW_OPENWEBUI_MODEL=openai/gpt-5.4-mini OPENWEBUI_SMOKE_MODE=models OPENCLAW_OPENWEBUI_PROVIDER_TIMEOUT_SECONDS=300 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui"; export const BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS = 24; const upgradeSurvivorCommand = "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:upgrade-survivor"; +const rootManagedVpsUpgradeCommand = + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:root-managed-vps-upgrade"; const updateRestartAuthCommand = "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-restart-auth"; @@ -345,6 +347,11 @@ export const mainLanes = [ weight: 3, }, ), + npmLane("root-managed-vps-upgrade", rootManagedVpsUpgradeCommand, { + stateScenario: "upgrade-survivor", + timeoutMs: 25 * 60 * 1000, + weight: 3, + }), npmLane("update-restart-auth", updateRestartAuthCommand, { stateScenario: "upgrade-survivor", timeoutMs: 25 * 60 * 1000, @@ -686,6 +693,11 @@ const releasePathPackageUpdateCoreLanes = [ weight: 3, }, ), + npmLane("root-managed-vps-upgrade", rootManagedVpsUpgradeCommand, { + stateScenario: "upgrade-survivor", + timeoutMs: 25 * 60 * 1000, + weight: 3, + }), npmLane("update-restart-auth", updateRestartAuthCommand, { stateScenario: "upgrade-survivor", timeoutMs: 25 * 60 * 1000, diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index e806794b4e2..2a02e4c5337 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -330,6 +330,16 @@ describe("scripts/lib/docker-e2e-plan", () => { timeoutMs: 1_500_000, weight: 3, }, + { + command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:root-managed-vps-upgrade", + imageKind: "bare", + live: false, + name: "root-managed-vps-upgrade", + resources: ["docker", "npm"], + stateScenario: "upgrade-survivor", + timeoutMs: 1_500_000, + weight: 3, + }, { command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-restart-auth", imageKind: "bare", @@ -483,6 +493,7 @@ describe("scripts/lib/docker-e2e-plan", () => { "skill-install", "upgrade-survivor", "published-upgrade-survivor", + "root-managed-vps-upgrade", "update-restart-auth", ]); expect(pluginsRuntime.lanes.map((lane) => lane.name)).toEqual([ diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 553fd01e983..9cfe6e024b3 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -115,7 +115,7 @@ describe("package acceptance workflow", () => { expect(workflow).toContain("update-channel-switch skill-install update-corrupt-plugin"); expect(workflow).toContain("update-corrupt-plugin upgrade-survivor"); expect(workflow).toContain("published-upgrade-survivor"); - expect(workflow).toContain("published-upgrade-survivor update-restart-auth"); + expect(workflow).toContain("published-upgrade-survivor root-managed-vps-upgrade update-restart-auth"); expect(workflow).toContain("plugins-offline plugin-update"); expect(workflow).toContain("include_release_path_suites=true"); expect(workflow).not.toContain("telegram_mode requires source=npm"); @@ -596,7 +596,7 @@ describe("package artifact reuse", () => { ); expect(workflow).toContain("suite_profile: custom"); expect(workflow).toContain( - "docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update", + "docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update", ); expect(workflow).toContain( "published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}",