mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(install): defer success until verification (#126871)
This commit is contained in:
committed by
GitHub
parent
fa71a6f27b
commit
e78b9d3ce4
+92
-92
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user