fix(install): defer success until verification (#126871)

This commit is contained in:
Peter Steinberger
2026-08-20 15:57:11 -07:00
committed by GitHub
parent fa71a6f27b
commit e78b9d3ce4
2 changed files with 349 additions and 92 deletions
+92 -92
View File
@@ -3752,7 +3752,9 @@ main() {
local is_upgrade=false
if check_existing_openclaw; then
is_upgrade=true
VERIFY_INSTALL=1
fi
configure_install_stage_total
local should_open_dashboard=false
ui_stage "Preparing environment"
@@ -3825,21 +3827,103 @@ main() {
fi
fi
local config_present=false
local config_present=false defer_success=false
if has_openclaw_config; then
config_present=true
refresh_gateway_service_if_loaded
fi
local installed_version=""
if [[ "$is_upgrade" != "true" ]]; then
installed_version="$(resolve_openclaw_version)"
echo ""
if [[ -n "$installed_version" ]]; then
ui_celebrate "🦞 OpenClaw installed successfully (${installed_version})!"
if [[ "$is_upgrade" == "true" || "$config_present" == "true" || "$VERIFY_INSTALL" == "1" ]]; then
defer_success=true
fi
if [[ "$config_present" == "true" && "$is_upgrade" == "true" ]]; then
if has_controlling_tty || [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
local claw="${OPENCLAW_BIN:-}"
if [[ -z "$claw" ]]; then
claw="$(resolve_installed_openclaw_bin || true)"
fi
if [[ -z "$claw" ]]; then
ui_info "Skipping doctor (openclaw not on PATH yet)"
warn_openclaw_not_found
return 0
fi
local -a doctor_args=("--fix")
if [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
doctor_args+=("--non-interactive")
fi
ui_info "Running openclaw doctor"
local doctor_exit=0
if [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor "${doctor_args[@]}" </dev/null || doctor_exit=$?
else
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor "${doctor_args[@]}" </dev/tty || doctor_exit=$?
fi
if (( doctor_exit == 130 )); then
abort_install_int
fi
if (( doctor_exit != 0 )); then
ui_warn "Doctor failed; skipping plugin updates"
return "$doctor_exit"
fi
should_open_dashboard=true
ui_info "Updating plugins"
OPENCLAW_UPDATE_IN_PROGRESS=1 run_with_safe_stdin "$claw" plugins update --all || true
else
ui_celebrate "🦞 OpenClaw installed successfully!"
run_doctor || return $?
should_open_dashboard=true
local user_claw
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
ui_info "No TTY; run ${user_claw} plugins update --all manually"
fi
elif [[ "$config_present" == "true" ]]; then
ui_info "Config already present; running doctor"
run_doctor || return $?
should_open_dashboard=true
ui_info "Config already present; skipping onboarding"
fi
if [[ "$config_present" == "true" ]]; then
local claw="${OPENCLAW_BIN:-}"
if [[ -z "$claw" ]]; then
claw="$(resolve_installed_openclaw_bin || true)"
fi
if [[ -n "$claw" ]] && is_gateway_daemon_loaded "$claw"; then
local user_claw
user_claw="$(openclaw_command_for_user "$claw")"
if [[ "$DRY_RUN" == "1" ]]; then
ui_info "Gateway daemon detected; would restart (${user_claw} daemon restart)"
else
ui_info "Gateway daemon detected; restarting"
if OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" daemon restart < /dev/null >/dev/null 2>&1; then
ui_success "Gateway restarted"
else
ui_warn "Gateway restart failed; try: ${user_claw} daemon restart"
fi
fi
fi
fi
if [[ "$defer_success" == "true" ]] && ! verify_installation "$config_present"; then
if [[ "$config_present" != "true" && "$NO_ONBOARD" != "1" ]] && ! is_promptable; then
local user_claw
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
ui_info "No TTY; run ${user_claw} onboard to finish setup"
fi
return 1
fi
local installed_version=""
installed_version="$(resolve_openclaw_version)"
echo ""
if [[ -n "$installed_version" ]]; then
ui_celebrate "🦞 OpenClaw installed successfully (${installed_version})!"
else
ui_celebrate "🦞 OpenClaw installed successfully!"
fi
if [[ "$is_upgrade" == "true" ]]; then
ui_info "Upgrade complete"
else
local completion_messages=(
"Ahh nice, I like it here. Got any snacks? "
"Home sweet home. Don't worry, I won't rearrange the furniture."
@@ -3893,89 +3977,6 @@ main() {
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
ui_info "No TTY; run ${user_claw} onboard to finish setup"
fi
elif [[ "$is_upgrade" == "true" ]]; then
if has_controlling_tty || [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
local claw="${OPENCLAW_BIN:-}"
if [[ -z "$claw" ]]; then
claw="$(resolve_installed_openclaw_bin || true)"
fi
if [[ -z "$claw" ]]; then
ui_info "Skipping doctor (openclaw not on PATH yet)"
warn_openclaw_not_found
return 0
fi
local -a doctor_args=("--fix")
if [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
doctor_args+=("--non-interactive")
fi
ui_info "Running openclaw doctor"
local doctor_exit=0
if [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor "${doctor_args[@]}" </dev/null || doctor_exit=$?
else
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor "${doctor_args[@]}" </dev/tty || doctor_exit=$?
fi
if (( doctor_exit == 130 )); then
abort_install_int
fi
if (( doctor_exit != 0 )); then
ui_warn "Doctor failed; skipping plugin updates"
return "$doctor_exit"
fi
should_open_dashboard=true
ui_info "Updating plugins"
OPENCLAW_UPDATE_IN_PROGRESS=1 run_with_safe_stdin "$claw" plugins update --all || true
else
run_doctor || return $?
should_open_dashboard=true
local user_claw
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
ui_info "No TTY; run ${user_claw} plugins update --all manually"
fi
else
ui_info "Config already present; running doctor"
run_doctor || return $?
should_open_dashboard=true
ui_info "Config already present; skipping onboarding"
fi
if [[ "$config_present" == "true" ]]; then
local claw="${OPENCLAW_BIN:-}"
if [[ -z "$claw" ]]; then
claw="$(resolve_installed_openclaw_bin || true)"
fi
if [[ -n "$claw" ]] && is_gateway_daemon_loaded "$claw"; then
local user_claw
user_claw="$(openclaw_command_for_user "$claw")"
if [[ "$DRY_RUN" == "1" ]]; then
ui_info "Gateway daemon detected; would restart (${user_claw} daemon restart)"
else
ui_info "Gateway daemon detected; restarting"
if OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" daemon restart < /dev/null >/dev/null 2>&1; then
ui_success "Gateway restarted"
else
ui_warn "Gateway restart failed; try: ${user_claw} daemon restart"
fi
fi
fi
fi
if [[ "$is_upgrade" == "true" ]]; then
VERIFY_INSTALL=1
fi
if ! verify_installation "$config_present"; then
exit 1
fi
if [[ "$is_upgrade" == "true" ]]; then
installed_version="$(resolve_openclaw_version)"
echo ""
if [[ -n "$installed_version" ]]; then
ui_celebrate "🦞 OpenClaw installed successfully (${installed_version})!"
else
ui_celebrate "🦞 OpenClaw installed successfully!"
fi
ui_info "Upgrade complete"
fi
if [[ "$should_open_dashboard" == "true" ]]; then
@@ -3987,7 +3988,6 @@ main() {
if [[ "${OPENCLAW_INSTALL_SH_NO_RUN:-0}" != "1" ]]; then
parse_args "$@"
configure_install_stage_total
configure_verbose
main
fi
+257
View File
@@ -1716,6 +1716,263 @@ EOF
expect(result.stdout).not.toContain("Upgrade complete");
});
it.each([
{
name: "fresh retained config rejects failed Doctor before success",
configured: true,
upgrade: false,
verify: false,
doctorExit: 9,
verifyExit: 0,
onboard: false,
expectedStatus: 9,
},
{
name: "fresh retained config reports success only after Doctor",
configured: true,
upgrade: false,
verify: false,
doctorExit: 0,
verifyExit: 0,
onboard: false,
expectedStatus: 0,
},
{
name: "fresh explicit verification rejects failure before success",
configured: false,
upgrade: false,
verify: true,
doctorExit: 0,
verifyExit: 1,
onboard: false,
expectedStatus: 1,
},
{
name: "fresh explicit verification reports success only after verification",
configured: false,
upgrade: false,
verify: true,
doctorExit: 0,
verifyExit: 0,
onboard: false,
expectedStatus: 0,
},
{
name: "upgrade implicit verification counts four stages before success",
configured: true,
upgrade: true,
verify: false,
doctorExit: 0,
verifyExit: 0,
onboard: false,
expectedStatus: 0,
},
{
name: "upgrade rejects failed Doctor before success",
configured: true,
upgrade: true,
verify: false,
doctorExit: 9,
verifyExit: 0,
onboard: false,
expectedStatus: 9,
},
{
name: "upgrade rejects failed verification before success",
configured: true,
upgrade: true,
verify: false,
doctorExit: 0,
verifyExit: 1,
onboard: false,
expectedStatus: 1,
},
{
name: "plain fresh install reports success before skipping onboarding",
configured: false,
upgrade: false,
verify: false,
doctorExit: 0,
verifyExit: 0,
onboard: false,
expectedStatus: 0,
},
{
name: "plain fresh install reports success before optional onboarding handoff",
configured: false,
upgrade: false,
verify: false,
doctorExit: 0,
verifyExit: 0,
onboard: true,
expectedStatus: 0,
},
{
name: "fresh verification completes before success and optional onboarding handoff",
configured: false,
upgrade: false,
verify: true,
doctorExit: 0,
verifyExit: 0,
onboard: true,
expectedStatus: 0,
},
])(
"required installer lifecycle: $name",
({ configured, upgrade, verify, doctorExit, verifyExit, onboard, expectedStatus }) => {
const result = runInstallShell(
`
date() { printf '2026-08-20\\n'; }
dirname() { printf 'scripts\\n'; }
PATH=/__openclaw_installer_test_no_external_commands__
source "${SCRIPT_PATH}"
cleanup_tmpfiles() { :; }
INSTALL_METHOD=git
GIT_DIR=
NO_PROMPT=0
NO_ONBOARD="$SCENARIO_NO_ONBOARD"
VERIFY_INSTALL="$SCENARIO_VERIFY"
OS=linux
forbidden_command() {
printf 'forbidden external command: %s\\n' "$1" >&2
return 98
}
launchctl() { forbidden_command launchctl; }
systemctl() { forbidden_command systemctl; }
schtasks() { forbidden_command schtasks; }
sudo() { forbidden_command sudo; }
curl() { forbidden_command curl; }
wget() { forbidden_command wget; }
brew() { forbidden_command brew; }
git() { forbidden_command git; }
node() { forbidden_command node; }
openclaw() { forbidden_command openclaw; }
run_quiet_step() { forbidden_command run_quiet_step; }
run_with_safe_stdin() { forbidden_command run_with_safe_stdin; }
install_homebrew() { forbidden_command install_homebrew; }
install_node() { forbidden_command install_node; }
install_git() { forbidden_command install_git; }
bootstrap_gum_temp() { :; }
print_installer_banner() { :; }
print_gum_status() { :; }
detect_os_or_die() { OS=linux; }
detect_openclaw_checkout() { return 1; }
show_install_plan() { :; }
check_existing_openclaw() { [[ "$SCENARIO_UPGRADE" == 1 ]]; }
load_nvm_for_node_detection() { :; }
check_node() { return 0; }
activate_supported_node_on_path() { :; }
ensure_default_node_active_shell() { return 0; }
npm() { return 1; }
install_openclaw_from_git() { printf 'event:installed\\n'; }
resolve_installed_openclaw_bin() { printf '/nonexistent/mock-openclaw\\n'; }
warn_duplicate_openclaw_global_installs() { :; }
npm_global_bin_dir() { :; }
warn_shell_path_missing_dir() { :; }
has_openclaw_config() { [[ "$SCENARIO_CONFIGURED" == 1 ]]; }
refresh_gateway_service_if_loaded() { printf 'event:service-refresh-mocked\\n'; }
has_controlling_tty() { return 1; }
is_gateway_daemon_loaded() { return 1; }
is_promptable() {
printf 'event:onboarding-handoff-probe\\n'
return 1
}
run_doctor() {
printf 'event:doctor\\n'
return "$SCENARIO_DOCTOR_EXIT"
}
resolve_openclaw_version() { printf '2026.8.20-test\\n'; }
verify_installation() {
[[ "$VERIFY_INSTALL" == 1 ]] || return 0
ui_stage "Verifying installation"
printf 'event:verification\\n'
return "$SCENARIO_VERIFY_EXIT"
}
maybe_open_dashboard() { printf 'event:dashboard-mocked\\n'; }
show_footer_links() { printf 'event:footer\\n'; }
ui_section() { printf 'event:stage:%s\\n' "$1"; }
ui_info() { printf 'event:info:%s\\n' "$*"; }
ui_celebrate() { printf 'event:success:%s\\n' "$*"; }
configure_install_stage_total
main
`,
{
OPENCLAW_CONFIG_PATH: "",
OPENCLAW_HOME: "",
OPENCLAW_STATE_DIR: "",
OPENCLAW_INSTALL_METHOD: "",
OPENCLAW_VERIFY_INSTALL: "0",
OPENCLAW_NO_ONBOARD: "0",
OPENCLAW_NO_PROMPT: "0",
SCENARIO_CONFIGURED: configured ? "1" : "0",
SCENARIO_UPGRADE: upgrade ? "1" : "0",
SCENARIO_VERIFY: verify ? "1" : "0",
SCENARIO_DOCTOR_EXIT: String(doctorExit),
SCENARIO_VERIFY_EXIT: String(verifyExit),
SCENARIO_NO_ONBOARD: onboard || configured ? "0" : "1",
TERM: "dumb",
},
);
expect(result.status, result.stderr || result.stdout).toBe(expectedStatus);
expect(result.stderr).not.toContain("forbidden external command");
const output = result.stdout;
const successMatches = output.match(/OpenClaw installed successfully/g) ?? [];
const doctorIndex = output.indexOf("event:doctor");
const verificationIndex = output.indexOf("event:verification");
const successIndex = output.indexOf("event:success:");
if (expectedStatus !== 0) {
expect(successMatches).toHaveLength(0);
expect(output).not.toContain("Upgrade complete");
return;
}
expect(successMatches).toHaveLength(1);
if (configured) {
expect(doctorIndex).toBeGreaterThan(-1);
expect(doctorIndex).toBeLessThan(successIndex);
} else {
expect(doctorIndex).toBe(-1);
}
if (verify || upgrade) {
expect(verificationIndex).toBeGreaterThan(-1);
expect(verificationIndex).toBeLessThan(successIndex);
expect(output).toContain("[4/4] Verifying installation");
expect(output).not.toContain("[4/3] Verifying installation");
} else {
expect(verificationIndex).toBe(-1);
expect(output).toContain("[3/3] Finalizing setup");
}
if (upgrade) {
const upgradeCompletionIndex = output.indexOf("event:info:Upgrade complete");
expect(upgradeCompletionIndex).toBeGreaterThan(successIndex);
} else {
expect(output).not.toContain("Upgrade complete");
}
if (onboard) {
const setupIndex = output.indexOf("event:info:Starting setup");
const handoffProbeIndex = output.indexOf("event:onboarding-handoff-probe");
expect(setupIndex).toBeGreaterThan(successIndex);
expect(handoffProbeIndex).toBeGreaterThan(setupIndex);
} else if (!configured) {
expect(output.indexOf("event:info:Skipping onboard")).toBeGreaterThan(successIndex);
}
},
);
it("required installer lifecycle: preserves the interactive exec onboarding handoff", () => {
expect(script).toMatch(/exec <\/dev\/tty\s+exec "\$claw" onboard/);
});
it("keeps the npm owner runnable when a npm-to-git candidate fails", () => {
const result = runInstallShell(`
source "${SCRIPT_PATH}"