diff --git a/bin/omarchy-default-agent b/bin/omarchy-default-agent index 684baaf2..0fe9a237 100755 --- a/bin/omarchy-default-agent +++ b/bin/omarchy-default-agent @@ -50,10 +50,11 @@ esac agent_package=${agent_package:-$agent} -# Hermes and OpenClaw are not mise tools: Hermes is the desktop app's -# self-updating runtime and OpenClaw comes from its pacman package. Each has -# its own installer with the same --check/--now contract mise-backed agents -# get from mise; see omarchy-install-hermes-cli and omarchy-install-openclaw-cli. +# Hermes and OpenClaw are not mise tools: each is a self-updating runtime in +# the user's home, Hermes the desktop app's and OpenClaw the one its Control +# UI updates. Each has its own installer with the same --check/--now contract +# mise-backed agents get from mise; see omarchy-install-hermes-cli and +# omarchy-install-openclaw-cli. if [[ -n ${agent_installer:-} ]]; then # Not omarchy-cmd-present: a command on PATH says nothing about whether the # install behind it finished or runs the sessions omarchy-agent starts. diff --git a/bin/omarchy-install-ai-openclaw b/bin/omarchy-install-ai-openclaw index cb79a9ad..978fab8c 100755 --- a/bin/omarchy-install-ai-openclaw +++ b/bin/omarchy-install-ai-openclaw @@ -5,8 +5,8 @@ set -e -echo "Installing OpenClaw..." -omarchy-pkg-add openclaw +# The same self-updating install the default agent gets. +omarchy-install-openclaw-cli --now # The desktop app is OpenClaw's Control UI: a web app served by its own # gateway. The launcher entry goes through omarchy-launch-openclaw, which @@ -14,7 +14,7 @@ omarchy-pkg-add openclaw # inside the package, so nothing is fetched here. echo "Installing the OpenClaw web app..." omarchy-webapp-install OpenClaw "http://127.0.0.1:18789" \ - /usr/lib/node_modules/openclaw/dist/control-ui/apple-touch-icon.png \ + /usr/share/openclaw/openclaw.png \ omarchy-launch-openclaw # A first install runs onboarding right here: launching the app instead would diff --git a/bin/omarchy-install-openclaw-cli b/bin/omarchy-install-openclaw-cli index 18e12317..17e5d0eb 100755 --- a/bin/omarchy-install-openclaw-cli +++ b/bin/omarchy-install-openclaw-cli @@ -1,29 +1,271 @@ #!/bin/bash -# omarchy:summary=Ensure the OpenClaw CLI is installed for the default agent -# omarchy:args=[--check|--now] +# omarchy:summary=Install OpenClaw for the default agent as the self-updating copy under ~/.openclaw +# omarchy:args=<--check|--now> +# omarchy:examples=omarchy install openclaw cli --now | omarchy install openclaw cli --check # omarchy:requires-sudo=true -# OpenClaw is not a mise tool: the openclaw pacman package is the one OpenClaw -# installation on the machine — the CLI, the gateway, and the Install > AI web -# app all share it, and the fast ring keeps it current. The default-agent flow -# talks to that package through the same --check/--now contract mise-backed -# agents get from mise itself. +# There is one OpenClaw on a machine, and it is the one under ~/.openclaw that +# upstream's install-cli.sh makes: a private Node and the package in a prefix +# the user owns, which `openclaw update` and the Control UI's Update button +# replace in place. A copy in /usr belongs to pacman, and upstream's updater +# refuses to touch it. The openclaw package is therefore the seed rather than +# the runtime: the release Omarchy ships and the installer that came with it. +# The CLI, the gateway service and the Install > AI web app all run this copy. +# +# Each mode is named outright, like omarchy-install-hermes-cli: the default +# agent asks --check first and opens a terminal for --now only on a no. set -euo pipefail -case "${1:---now}" in ---check) - omarchy-pkg-present openclaw - ;; ---now) - if ! omarchy-pkg-present openclaw; then - echo "Installing OpenClaw..." - omarchy-pkg-add openclaw +mode=${1:-} + +# Everything here runs the user's own files, --check included. +if (( EUID == 0 )); then + echo "Run this command as your desktop user, without sudo." >&2 + exit 1 +fi + +# Omarchy's OpenClaw is the default one, in ~/.openclaw with the default unit, +# whatever another profile, unit or state directory a shell selects; a service +# installed from here would otherwise carry that selection. +unset OPENCLAW_PROFILE OPENCLAW_SYSTEMD_UNIT OPENCLAW_WRAPPER OPENCLAW_HOME OPENCLAW_STATE_DIR OPENCLAW_CONFIG_PATH + +prefix="$HOME/.openclaw" +runtime_command="$prefix/bin/openclaw" +command_path="$HOME/.local/bin/openclaw" +seed=/usr/share/openclaw + +# Usable means the command answers, not that the file is there: upstream's +# wrapper execs the prefix's own Node, so a prefix missing either fails here. +runtime_runs() { + [[ -f $runtime_command && -x $runtime_command ]] && + timeout 30 "$runtime_command" --version >/dev/null 2>&1 +} + +# The name on PATH is a link to the runtime's command. A link already aimed +# there, even one left dangling by a removed runtime, is Omarchy's to rewrite; +# anything else at the path is the user's. +command_ours() { + [[ ! -e $command_path && ! -L $command_path ]] || [[ $(readlink -- "$command_path") == "$runtime_command" ]] +} + +# What omarchy-agent, the web app and a terminal run is whichever openclaw is +# first on PATH, and Omarchy puts /usr/bin and the mise shims ahead of +# ~/.local/bin, so the command has to resolve to the runtime from there. +on_path() { + local found + found=$(type -P openclaw) || return 1 + [[ $(realpath -m -- "$found") == "$(realpath -m -- "$runtime_command")" ]] +} + +# Nothing yet, or the runtime's command. +path_clear() { + ! type -P openclaw >/dev/null || on_path +} + +# The package counts too: without it --now has a pacman step to take, and +# answering yes here would run that where no terminal can ask for a password. +installed() { + omarchy-pkg-present openclaw && runtime_runs && on_path +} + +# A unit's ExecStart, as the program and the script it is handed: the first +# argument that is not an option. Later arguments and other lines can name +# ~/.openclaw whichever OpenClaw the unit starts. +unit_program() { + local words + read -ra words <<<"$(sed -n 's/^ExecStart=//p' "$1" | head -1)" + echo "${words[0]:-}" +} + +unit_script() { + local words word + read -ra words <<<"$(sed -n 's/^ExecStart=//p' "$1" | head -1)" + for word in "${words[@]:1}"; do + if [[ $word != -* ]]; then + echo "$word" + return + fi + done +} + +# The runtime's own OpenClaw, as upstream's installer lays it out. +runs_runtime() { + [[ $(unit_script "$1") == "$prefix"/tools/*/lib/node_modules/openclaw/* ]] +} + +# A gateway service already running some other OpenClaw. Upstream's installer +# rewrites a loaded gateway service to the copy it has just installed, so +# seeding beside one would take it over. +foreign_gateway() { + local unit + unit=$(unit_path gateway) + [[ -f $unit ]] && ! runs_runtime "$unit" && ! on_old_package "$unit" +} + +# A service the openclaw package's own copy installed runs from +# /usr/lib/node_modules/openclaw, which that package no longer ships, and is +# moved to the runtime, where `openclaw update` can replace its code. A service +# running any other OpenClaw is the user's arrangement and stays. The --now +# steps below list these, and which were running, before anything changes. +unit_path() { + echo "$HOME/.config/systemd/user/openclaw-$1.service" +} + +# Stopped before anything is installed: still running the old code from deleted +# files, such a service holds the state directory the newer runtime has to +# migrate, which upstream's installer does the moment it finds one loaded. +stop_legacy_services() { + local role + for role in "${moving[@]}"; do + if ! systemctl --user stop "openclaw-$role.service"; then + echo "Could not stop the OpenClaw $role service, so OpenClaw was not moved to $prefix." >&2 + return 1 + fi + if [[ " ${running[*]} " == *" $role "* ]]; then + stopped+=("$role") + fi + done +} + +# Still the old package's: its script, or the runtime's script on the old +# package's Node. Upstream's installer keeps the Node a service already ran, so +# a moved unit can end up running the runtime's OpenClaw on /usr/bin/node, from +# the nodejs package the seed no longer depends on. Any other Node, Bun or +# script is the user's. +on_old_package() { + [[ $(unit_script "$1") == /usr/lib/node_modules/openclaw/* ]] || + { [[ $(unit_program "$1") == "/usr/bin/node" ]] && runs_runtime "$1"; } +} + +# Not moved yet: still on the old package, or running before and not now. +unsettled() { + on_old_package "$(unit_path "$1")" || + { [[ " ${running[*]} " == *" $1 "* ]] && ! systemctl --user is-active --quiet "openclaw-$1.service"; } +} + +# Upstream's installer rewrites a loaded gateway itself, but only warns when +# the restart fails, so whatever is still unsettled is installed again from the +# runtime, on the runtime's own Node, and then has to have settled. +rehome_services() { + local role node pin=() + if node=$(realpath -e -- "$prefix/tools/node/bin/node" 2>/dev/null); then + pin=(--runtime-path "$node") fi + for role in "${moving[@]}"; do + if unsettled "$role"; then + echo "Moving the OpenClaw $role service to $prefix..." + "$runtime_command" "$role" install --force "${pin[@]}" || true + fi + if unsettled "$role"; then + echo "Could not move the OpenClaw $role service to $prefix. Finish with: openclaw $role install --force" >&2 + return 1 + fi + # Installing a service enables and starts it; one the user had left + # stopped or disabled goes back to that. + if [[ " ${running[*]} " != *" $role "* ]]; then + systemctl --user stop "openclaw-$role.service" || true + fi + if [[ " ${enabled[*]} " != *" $role "* ]]; then + systemctl --user disable "openclaw-$role.service" 2>/dev/null || true + fi + done +} + +# Said on any failure that leaves a service this run stopped not running. +stopped_note() { + local role + for role in "${stopped[@]}"; do + if ! systemctl --user is-active --quiet "openclaw-$role.service"; then + echo "The OpenClaw $role service was stopped for this and is not running now." >&2 + fi + done +} + +case "$mode" in +--check) + if installed; then exit 0; else exit 1; fi ;; +--now) ;; *) - echo "Usage: omarchy-install-openclaw-cli [--check|--now]" >&2 + echo "Usage: omarchy-install-openclaw-cli <--check|--now>" >&2 exit 1 ;; esac + +# Refused before anything is installed or touched: the command's name, what +# PATH finds, and a gateway running another OpenClaw all have to be clear. +if ! command_ours; then + echo "$command_path is not the OpenClaw Omarchy set up. Move it aside, then run omarchy-install-openclaw-cli --now again." >&2 + exit 1 +fi +if ! path_clear; then + echo "'openclaw' on PATH is $(type -P openclaw), which is what Omarchy would run instead of $runtime_command." >&2 + echo "Remove it or reorder PATH, then run omarchy-install-openclaw-cli --now again." >&2 + exit 1 +fi + +# A runtime that already answers is never reinstalled: it may be past the +# packaged release by its own updates, and seeding would take it back. +seeding=false +if ! runtime_runs; then + seeding=true +fi +if [[ $seeding == "true" ]] && foreign_gateway; then + echo "The OpenClaw gateway service runs another OpenClaw, which setting one up in $prefix would take over." >&2 + echo "Remove that gateway with 'openclaw gateway uninstall', then run omarchy-install-openclaw-cli --now again." >&2 + exit 1 +fi + +if ! omarchy-pkg-present openclaw; then + echo "Installing OpenClaw..." + omarchy-pkg-add openclaw +fi +if [[ $seeding == "true" && ( ! -r $seed/install-cli.sh || ! -r $seed/openclaw.tgz ) ]]; then + echo "The installed OpenClaw package cannot set up a self-updating OpenClaw. Run 'omarchy update', then try again." >&2 + exit 1 +fi + +# What the old package installed, and what of it was running and enabled, read +# before anything changes. +moving=() +running=() +enabled=() +stopped=() +for role in gateway node; do + if [[ -f $(unit_path "$role") ]] && on_old_package "$(unit_path "$role")"; then + moving+=("$role") + if systemctl --user is-active --quiet "openclaw-$role.service"; then + running+=("$role") + fi + if systemctl --user is-enabled --quiet "openclaw-$role.service"; then + enabled+=("$role") + fi + fi +done + +trap 'status=$?; (( status == 0 )) || stopped_note' EXIT +stop_legacy_services + +if [[ $seeding == "true" ]]; then + # Every choice the installer reads from the environment is named, so an + # OPENCLAW_INSTALL_METHOD or OPENCLAW_PREFIX left in a shell cannot move it. + echo "Setting up OpenClaw in $prefix..." + bash "$seed/install-cli.sh" --install-method npm --prefix "$prefix" --version "$seed/openclaw.tgz" --no-onboard || true + if ! runtime_runs; then + echo "OpenClaw setup did not complete. Re-run this command after resolving the installer error." >&2 + exit 1 + fi +fi + +mkdir -p "${command_path%/*}" +ln -sfn "$runtime_command" "$command_path" + +rehome_services + +if ! on_path; then + echo "$runtime_command is ready, but 'openclaw' on PATH is $(type -P openclaw || echo missing), which is what Omarchy runs." >&2 + echo "Put ~/.local/bin on PATH, then run omarchy-install-openclaw-cli --now again." >&2 + exit 1 +fi diff --git a/bin/omarchy-openclaw-onboard b/bin/omarchy-openclaw-onboard index 5bac15bd..d8665b3e 100755 --- a/bin/omarchy-openclaw-onboard +++ b/bin/omarchy-openclaw-onboard @@ -26,8 +26,12 @@ settle_seconds=${OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS:-3} # setup; the prompts before that take as long as the user takes. gateway_timeout=${OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT:-180} +# Probes never get the terminal. The wizard writes its config while prompts +# remain, a probe of a gateway not yet up then hangs until the timeout, and +# OpenClaw's CLI takes a terminal on stdin out of raw mode as it is killed, +# which leaves the wizard's next prompt echoing keys instead of reading them. gateway_answers() { - timeout 10 openclaw dashboard --json 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1 + timeout 10 openclaw dashboard --json /dev/null | jq -e '.ok == true' >/dev/null 2>&1 } # A gateway already answering means OpenClaw is set up, and this run must not @@ -81,7 +85,7 @@ config_applied() { gateway_ready() { config_applied || return 1 local json port listener main_pid - json=$(timeout 10 openclaw dashboard --json 2>/dev/null) || return 1 + json=$(timeout 10 openclaw dashboard --json /dev/null) || return 1 jq -e '.ok == true' <<<"$json" >/dev/null 2>&1 || return 1 port=$(jq -r '.port // empty' <<<"$json" 2>/dev/null) [[ -n $port ]] || return 1 diff --git a/bin/omarchy-remove-ai-openclaw b/bin/omarchy-remove-ai-openclaw index a5b15a23..2b9ea1a4 100755 --- a/bin/omarchy-remove-ai-openclaw +++ b/bin/omarchy-remove-ai-openclaw @@ -35,8 +35,9 @@ for unit_file in "$unit_dir"/openclaw-gateway.service "$unit_dir"/openclaw-node. systemctl --user disable --now "$unit" 2>/dev/null || unit_stopped "$unit"; then # .bak is what `gateway install --force` leaves behind when it rewrites a - # unit, so it goes with the unit. - rm -f "$unit_file" "$unit_file.bak" "$unit_dir/default.target.wants/$unit" + # unit, and .reconcile-*.bak what `openclaw update` leaves when it + # refreshes one, so they go with the unit. + rm -f "$unit_file" "$unit_file.bak" "$unit_file".reconcile-*.bak "$unit_dir/default.target.wants/$unit" systemctl --user daemon-reload 2>/dev/null || true # A unit that had been failing stays listed as "not-found failed" after # its file is gone until its failed state is reset. @@ -49,6 +50,14 @@ done omarchy-pkg-drop openclaw +# The command on PATH goes, so nothing answers to openclaw. The OpenClaw it +# pointed at stays in ~/.openclaw with the state below: its own updates may +# have migrated that state past the packaged release, which could not open it +# again, and a reinstall picks the kept copy back up instead of seeding one. +if [[ -L $HOME/.local/bin/openclaw && $(readlink -- "$HOME/.local/bin/openclaw") == "$HOME/.openclaw/bin/openclaw" ]]; then + rm -f "$HOME/.local/bin/openclaw" +fi + # The web app launcher and icon omarchy-install-ai-openclaw created. rm -f "$HOME/.local/share/applications/OpenClaw.desktop" rm -f "$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png" @@ -59,12 +68,13 @@ gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true # better surprise. But it also holds the plugin runtimes and caches OpenClaw # downloads for itself, easily hundreds of megabytes, so the choice is put in # front of the user with the size rather than left silent. Without a terminal -# to ask in, keeping it is the answer. +# to ask in, keeping it is the answer. The snapshots `openclaw update` takes +# of that state sit beside it and go with it. state_removed=false if [[ -d $HOME/.openclaw && -t 0 ]]; then size=$(du -sh "$HOME/.openclaw" 2>/dev/null | cut -f1) - if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, and downloaded plugins)?"; then - rm -rf "$HOME/.openclaw" + if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, downloaded plugins, and OpenClaw itself)?"; then + rm -rf "$HOME/.openclaw" "$HOME/.openclaw.update-captures" state_removed=true fi fi diff --git a/manual/17-ai.md b/manual/17-ai.md index ea8a8d65..3bcca3e1 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -66,7 +66,7 @@ The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT deskto Hermes Desktop is the one to know about, because a machine has one Hermes, and it is the app's: Hermes is only ever installed through the app, and the `hermes` command the default agent runs is the app's. Choosing Hermes as the default agent installs the same thing the _Install > AI_ entry does: the package, and a Hermes runtime under `~/.hermes` set up by Hermes' own installer, which takes a few minutes the first time. That runtime is the one Hermes the terminal `hermes` command, the default agent and the app all use, and it updates itself with `hermes update` rather than waiting on an Omarchy release. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. Removing the app under _Remove > AI_ closes Hermes first, gateway included, takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too. -OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw updates arrive through Omarchy's package updates, so skip the Control UI's own "Update Gateway" button: it would try to write into the package-managed install and fail. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside the plugin runtimes OpenClaw downloads for itself; the default keeps it. +OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw lives in `~/.openclaw` and updates itself: use the Control UI's "Update Gateway" button or run `openclaw update`, and the gateway restarts on the new version. Omarchy only chooses the release a fresh install starts from. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside OpenClaw itself and the plugin runtimes it downloads; the default keeps it, and installing OpenClaw again picks it back up. ### Local LLMs diff --git a/migrations/1790397381.sh b/migrations/1790397381.sh new file mode 100644 index 00000000..0103159c --- /dev/null +++ b/migrations/1790397381.sh @@ -0,0 +1,12 @@ +echo "Move OpenClaw to a self-updating install under ~/.openclaw" + +# The openclaw package used to be OpenClaw itself, installed under /usr where `openclaw update` and the Control UI's Update button cannot write. It is now the seed for a copy under ~/.openclaw that updates itself, and omarchy-install-openclaw-cli sets that copy up, points the command on PATH at it and moves a gateway service the old package installed over to it. Only machines with the package have an OpenClaw of Omarchy's to move. +omarchy-pkg-present openclaw || exit 0 + +# Updates install packages before migrations. An older package is still the runtime and has no seed, so this stays pending until it is updated. +if [[ ! -r /usr/share/openclaw/install-cli.sh || ! -r /usr/share/openclaw/openclaw.tgz ]]; then + echo "Update the openclaw package before rerunning this migration." >&2 + exit 1 +fi + +omarchy-install-openclaw-cli --now diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index e70405ea..09d4d834 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -874,15 +874,16 @@ grep -F "missing is not installed" "$test_tmp/missing-output" >/dev/null || fail "agent launcher explains when the default command is missing" pass "agent launcher reports a missing default command" -# OpenClaw comes from its pacman package, not mise: choosing it must route +# OpenClaw is its own self-updating runtime, not mise's: choosing it must route # through omarchy-install-openclaw-cli and never touch a mise environment. -cat >"$mock_bin/omarchy-pkg-present" <<'SH' +# openclaw-cli-test.sh covers the installer itself. +cat >"$mock_bin/omarchy-install-openclaw-cli" <<'SH' #!/bin/bash -[[ $1 == openclaw && ${OMARCHY_TEST_OPENCLAW_INSTALLED:-false} == "true" ]] -SH -cat >"$mock_bin/omarchy-pkg-add" <<'SH' -#!/bin/bash -printf '%s\n' "pkg-add $*" >>"$OMARCHY_TEST_STUB_LOG" +if [[ $1 == "--check" ]]; then + [[ ${OMARCHY_TEST_OPENCLAW_INSTALLED:-false} == "true" ]] +else + printf '%s\n' "install-openclaw-cli $*" >>"$OMARCHY_TEST_STUB_LOG" +fi SH cat >"$mock_bin/omarchy-launch-openclaw" <<'SH' #!/bin/bash @@ -892,7 +893,7 @@ cat >"$mock_bin/openclaw" <<'SH' #!/bin/bash exit 0 SH -chmod +x "$mock_bin/omarchy-pkg-present" "$mock_bin/omarchy-pkg-add" \ +chmod +x "$mock_bin/omarchy-install-openclaw-cli" \ "$mock_bin/omarchy-launch-openclaw" "$mock_bin/openclaw" : >"$launch_log" @@ -906,7 +907,7 @@ mapfile -d '' -t launch_args <"$launch_log" fail "choosing OpenClaw launches its terminal UI" [[ ! -s $terminal_log ]] || fail "an installed OpenClaw needs no install terminal" ! grep -q 'use -g openclaw' "$mise_history" || fail "OpenClaw never installs through mise" -pass "choosing OpenClaw uses the package and launches its terminal UI" +pass "choosing OpenClaw uses its runtime and launches its terminal UI" : >"$terminal_log" OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent openclaw @@ -918,12 +919,12 @@ pass "a missing OpenClaw routes through the install terminal" : >"$stub_log" : >"$inline_log" OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent --install openclaw >/dev/null -grep -Fx "pkg-add openclaw" "$stub_log" >/dev/null || - fail "installing OpenClaw as default agent adds its package" +grep -Fx "install-openclaw-cli --now" "$stub_log" >/dev/null || + fail "installing OpenClaw as default agent sets up its runtime" mapfile -d '' -t inline_args <"$inline_log" [[ ${inline_args[*]} == "omarchy-launch-openclaw --tui" ]] || fail "installing OpenClaw as default agent hands over to its terminal UI" -pass "installing OpenClaw as default agent adds its package" +pass "installing OpenClaw as default agent sets up its runtime" : >"$launch_log" omarchy agent prompt "Review this project" diff --git a/test/shell.d/openclaw-cli-test.sh b/test/shell.d/openclaw-cli-test.sh new file mode 100755 index 00000000..e31a971f --- /dev/null +++ b/test/shell.d/openclaw-cli-test.sh @@ -0,0 +1,333 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +mock_bin="$test_tmp/bin" +seed="$test_tmp/share" +events="$test_tmp/events" +mkdir -p "$mock_bin" "$seed" + +cat >"$mock_bin/omarchy-pkg-present" <<'SH' +#!/bin/bash +[[ $1 == openclaw && -e $OMARCHY_TEST_ROOT/package-installed ]] +SH +cat >"$mock_bin/omarchy-pkg-add" <<'SH' +#!/bin/bash +printf 'pkg-add %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" +touch "$OMARCHY_TEST_ROOT/package-installed" +SH +# The user manager, as far as these tests need one: a service is active while +# a marker says so, and enabled likewise. Stopping clears it, except for the +# unit named in OMARCHY_TEST_STOP_FAIL. +cat >"$mock_bin/systemctl" <<'SH' +#!/bin/bash +case "$2" in + stop) + printf 'systemctl %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" + [[ ${OMARCHY_TEST_STOP_FAIL:-} != "$3" ]] || exit 1 + rm -f "$HOME/active-$3" + ;; + is-active) [[ -e $HOME/active-$4 ]] ;; + is-enabled) [[ -e $HOME/enabled-$4 ]] ;; + disable) + printf 'systemctl %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events" + rm -f "$HOME/enabled-$3" + ;; +esac +SH +chmod +x "$mock_bin/"* + +# Stands in for upstream's install-cli.sh: it writes the command the way the +# real one does, execing into the prefix's tools, and that command logs what +# it is asked. OMARCHY_TEST_INSTALL_BROKEN leaves a command that cannot run. +# Like upstream, ` install --force` rewrites the unit onto the runtime +# and starts it, OMARCHY_TEST_START_FAIL making the start fail, and the +# installer does that itself for a gateway it finds loaded. As upstream does +# since 2026.9.6, a rewrite keeps the Node the unit already ran unless +# --runtime-path pins one. +cat >"$seed/install-cli.sh" <<'SH' +printf 'install-cli %s%s\n' "$*" "${OPENCLAW_PROFILE:+ profile=$OPENCLAW_PROFILE}" >>"$OMARCHY_TEST_ROOT/events" +prefix=$HOME/.openclaw +mkdir -p "$prefix/bin" "$prefix/tools/node-v24.19.0/bin" +touch "$prefix/tools/node-v24.19.0/bin/node" +ln -sfn "$prefix/tools/node-v24.19.0" "$prefix/tools/node" +cat >"$prefix/bin/openclaw" <>"$OMARCHY_TEST_ROOT/events" +if [[ \${2:-} == "install" ]]; then + unit="\$HOME/.config/systemd/user/openclaw-\$1.service" + node=$prefix/tools/node-v24.19.0/bin/node + if [[ \${4:-} == "--runtime-path" ]]; then + node=\$5 + elif [[ -f \$unit ]]; then + node=\$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "\$unit") + fi + printf 'ExecStart=%s $prefix/tools/node-v24.19.0/lib/node_modules/openclaw/dist/index.js %s\n' "\$node" "\$1" >"\$unit" + [[ -z "\${OMARCHY_TEST_START_FAIL:-}" ]] || exit 1 + touch "\$HOME/active-openclaw-\$1.service" "\$HOME/enabled-openclaw-\$1.service" +fi +exec true "$prefix/tools/node-v24.19.0/lib/node_modules/openclaw/dist/entry.js" "\$@" +EOF +chmod 755 "$prefix/bin/openclaw" +if [[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]]; then + "$prefix/bin/openclaw" gateway install --force || true +fi +SH +touch "$seed/openclaw.tgz" + +# Scratch copies of the actual scripts, with only the package's path swapped. +for script in bin/omarchy-install-openclaw-cli migrations/1790397381.sh; do + sed "s|/usr/share/openclaw|$seed|g" "$ROOT/$script" >"$mock_bin/${script##*/}" +done +mv "$mock_bin/1790397381.sh" "$test_tmp/migration.sh" +chmod +x "$mock_bin/omarchy-install-openclaw-cli" + +new_home() { + test_home="$test_tmp/$1" + runtime="$test_home/.openclaw/bin/openclaw" + command="$test_home/.local/bin/openclaw" + mkdir -p "$test_home/.local/bin" + rm -f "$test_tmp/package-installed" + : >"$events" +} + +# PATH puts a directory ahead of ~/.local/bin the way Omarchy's does, where a +# test can drop another openclaw. +mkdir -p "$test_tmp/usr-bin" +run() { + HOME="$test_home" OMARCHY_TEST_ROOT="$test_tmp" PATH="$test_tmp/usr-bin:$mock_bin:$test_home/.local/bin:$PATH" \ + "$@" >"$test_tmp/output" 2>&1 +} + +new_home usage +run omarchy-install-openclaw-cli && fail "no mode is a usage error" +[[ ! -s $events ]] || fail "no mode installs nothing" "$(cat "$events")" +pass "every mode is named outright" + +new_home fresh +run omarchy-install-openclaw-cli --check && fail "--check calls a machine without OpenClaw installed" +run omarchy-install-openclaw-cli --now || fail "--now sets OpenClaw up" "$(cat "$test_tmp/output")" +grep -Fxq "pkg-add openclaw" "$events" || fail "--now installs the package" "$(cat "$events")" +grep -Fxq "install-cli --install-method npm --prefix $test_home/.openclaw --version $seed/openclaw.tgz --no-onboard" "$events" || + fail "--now seeds the runtime from the packaged release" "$(cat "$events")" +[[ -L $command && $(readlink -- "$command") == "$runtime" ]] || fail "--now points the command on PATH at the runtime" +run omarchy-install-openclaw-cli --check || fail "--check follows a finished install" +pass "--now seeds a self-updating OpenClaw from the packaged release" + +: >"$events" +run omarchy-install-openclaw-cli --now || fail "--now accepts a finished install" "$(cat "$test_tmp/output")" +! grep -q '^install-cli\|^pkg-add' "$events" || fail "a runtime that answers is never reseeded" "$(cat "$events")" +pass "a runtime that answers is never reseeded, whatever version its updates reached" + +rm -f "$command" +ln -s "$runtime" "$command.tmp" && mv "$command.tmp" "$command" +mv "$test_home/.openclaw" "$test_home/.openclaw.gone" +run omarchy-install-openclaw-cli --check && fail "--check calls a dangling link installed" +run omarchy-install-openclaw-cli --now || fail "--now reseeds behind its own dangling link" "$(cat "$test_tmp/output")" +[[ -x $runtime && $(readlink -- "$command") == "$runtime" ]] || fail "--now reseeds behind its own dangling link" +pass "a link Omarchy left behind is rewritten, and a missing runtime reseeded" + +new_home old-package +touch "$test_tmp/package-installed" +mv "$seed" "$seed.old" +run omarchy-install-openclaw-cli --now && fail "a package with no seed cannot set OpenClaw up" +grep -q "Run 'omarchy update'" "$test_tmp/output" || fail "a package with no seed says to update" "$(cat "$test_tmp/output")" +[[ ! -e $test_home/.openclaw && ! -e $command ]] || fail "a package with no seed leaves the home untouched" +mv "$seed.old" "$seed" +pass "a package that is still the runtime is refused before anything is touched" + +new_home broken +OMARCHY_TEST_INSTALL_BROKEN=1 run omarchy-install-openclaw-cli --now && fail "a runtime that does not run is not a finished install" +grep -q "did not complete" "$test_tmp/output" || fail "a failed setup says so" "$(cat "$test_tmp/output")" +[[ ! -e $command ]] || fail "a failed setup puts nothing on PATH" +pass "a runtime that does not run fails the install" + +new_home foreign +printf '#!/bin/bash\necho mine\n' >"$command" +chmod +x "$command" +run omarchy-install-openclaw-cli --now && fail "a command that is the user's is not replaced" +grep -q "Move it aside" "$test_tmp/output" || fail "a command that is the user's is named" "$(cat "$test_tmp/output")" +[[ ! -L $command ]] && grep -q mine "$command" || fail "a command that is the user's is kept" +run omarchy-install-openclaw-cli --check && fail "--check calls the runtime installed while the command is somebody else's" +pass "a command at the path that is the user's is kept and named" + +new_home shadowed +printf '#!/bin/bash\n' >"$test_tmp/usr-bin/openclaw" +chmod +x "$test_tmp/usr-bin/openclaw" +run omarchy-install-openclaw-cli --now && fail "an openclaw earlier on PATH fails the install" +grep -q "on PATH is $test_tmp/usr-bin/openclaw" "$test_tmp/output" || fail "an openclaw earlier on PATH is named" "$(cat "$test_tmp/output")" +[[ ! -e $test_home/.openclaw && ! -e $command ]] || fail "an openclaw earlier on PATH is refused before anything is touched" +rm "$test_tmp/usr-bin/openclaw" +run omarchy-install-openclaw-cli --now || fail "--now follows once nothing shadows the runtime" "$(cat "$test_tmp/output")" +printf '#!/bin/bash\n' >"$test_tmp/usr-bin/openclaw" +chmod +x "$test_tmp/usr-bin/openclaw" +run omarchy-install-openclaw-cli --check && fail "--check calls a shadowed runtime installed" +rm "$test_tmp/usr-bin/openclaw" +run omarchy-install-openclaw-cli --check || fail "--check follows once nothing shadows the runtime" +pass "the runtime has to be the openclaw PATH finds, and anything else is refused before it is set up" + +# A runtime that answers without the package still leaves --now a pacman step, +# which the default agent must not run outside a terminal. +rm "$test_tmp/package-installed" +run omarchy-install-openclaw-cli --check && fail "--check calls a runtime without its package installed" +pass "--check needs the package too, so --now never has a password to ask for unseen" + +# Upstream's installer rewrites a loaded gateway service to the copy it has +# just made, so a gateway running another OpenClaw stops the seeding first. +new_home foreign-gateway +mkdir -p "$test_home/.config/systemd/user" +printf 'Environment=OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json\nExecStart=/opt/node %s/openclaw/dist/index.js gateway --config %s/.openclaw/openclaw.json\n' "$test_home" "$test_home" "$test_home" \ + >"$test_home/.config/systemd/user/openclaw-gateway.service" +run omarchy-install-openclaw-cli --now && fail "a gateway running another OpenClaw stops the install" +grep -q "runs another OpenClaw" "$test_tmp/output" || fail "a gateway running another OpenClaw is named" "$(cat "$test_tmp/output")" +! grep -q '^install-cli' "$events" && [[ ! -e $test_home/.openclaw ]] || + fail "a gateway running another OpenClaw is refused before anything is set up" "$(cat "$events")" +pass "a gateway running another OpenClaw is refused before upstream's installer can take it over" + +# A gateway the old package installed runs from /usr/lib/node_modules, which +# the seed package no longer ships; one running any other OpenClaw stays. +new_home services +units="$test_home/.config/systemd/user" +mkdir -p "$units" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$units/openclaw-gateway.service" +printf 'ExecStart=/opt/node /home/someone/openclaw/dist/index.js node run\n' >"$units/openclaw-node.service" +touch "$test_home/active-openclaw-gateway.service" "$test_home/enabled-openclaw-gateway.service" +OPENCLAW_PROFILE=work run omarchy-install-openclaw-cli --now || fail "--now moves the old package's services" "$(cat "$test_tmp/output")" +order=$(grep -n -e '^systemctl --user stop openclaw-gateway.service$' -e '^install-cli ' -e '^runtime gateway install --force$' "$events" | cut -d: -f2- | cut -c1-11) +[[ $order == $'systemctl -\ninstall-cli\nruntime gat' ]] || + fail "--now stops a gateway the old package installed before seeding, then moves it" "$(cat "$events")" +! grep -q "runtime node install\|openclaw-node" "$events" || fail "--now leaves a service running another OpenClaw alone" "$(cat "$events")" +! grep -q "profile=work" <(grep -v -e '^runtime --version' "$events") || + fail "--now seeds and moves the default unit whatever profile the shell selects" "$(cat "$events")" +sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$units/openclaw-gateway.service" | grep -qx "$test_home/.openclaw/tools/node-v24.19.0/bin/node" || + fail "--now leaves the moved gateway on the runtime's own Node" "$(cat "$units/openclaw-gateway.service")" +grep -Fxq "runtime gateway install --force --runtime-path $test_home/.openclaw/tools/node-v24.19.0/bin/node" "$events" || + fail "--now pins the runtime's Node when the installer kept the system one" "$(cat "$events")" +pass "a service the old package installed moves to the runtime, and only that one" + +# Installing a service enables and starts it, so one the user had left stopped +# and disabled is moved and then put back that way. +new_home dormant +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +run omarchy-install-openclaw-cli --now || fail "--now moves a dormant gateway" "$(cat "$test_tmp/output")" +runs=$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$test_home/.config/systemd/user/openclaw-gateway.service") +[[ $runs == "$test_home/.openclaw/tools/node-v24.19.0/bin/node" && ! -e $test_home/active-openclaw-gateway.service && ! -e $test_home/enabled-openclaw-gateway.service ]] || + fail "--now moves a dormant gateway and leaves it stopped and disabled" "$(cat "$events")" +pass "a gateway the user left stopped and disabled is moved and stays that way" + +new_home stop-fails +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +OMARCHY_TEST_STOP_FAIL=openclaw-gateway.service run omarchy-install-openclaw-cli --now && fail "a gateway that will not stop stops the install" +grep -q "Could not stop the OpenClaw gateway service" "$test_tmp/output" || fail "a gateway that will not stop is named" "$(cat "$test_tmp/output")" +! grep -q '^install-cli' "$events" && [[ ! -e $test_home/.openclaw ]] || + fail "a gateway that will not stop leaves the runtime unseeded" "$(cat "$events")" +pass "a gateway that will not stop stops the install before anything is seeded" + +new_home stop-partial +mkdir -p "$test_home/.config/systemd/user" +for role in gateway node; do + printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js %s\n' "$role" >"$test_home/.config/systemd/user/openclaw-$role.service" + touch "$test_home/active-openclaw-$role.service" +done +OMARCHY_TEST_STOP_FAIL=openclaw-node.service run omarchy-install-openclaw-cli --now && fail "a node host that will not stop stops the install" +grep -q "gateway service was stopped for this and is not running now" "$test_tmp/output" || + fail "a gateway stopped before a later stop failed is named as stopped" "$(cat "$test_tmp/output")" +pass "a service this run stopped is named whenever the run then fails" + +# Upstream's installer rewrites the gateway itself and only warns when it will +# not start again, so a gateway that was running has to be running afterwards. +new_home start-fails +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +touch "$test_home/active-openclaw-gateway.service" +OMARCHY_TEST_START_FAIL=1 run omarchy-install-openclaw-cli --now && fail "a moved gateway that does not start fails the install" +grep -q "Could not move the OpenClaw gateway service" "$test_tmp/output" && grep -q "is not running now" "$test_tmp/output" || + fail "a moved gateway that does not start is named, and so is its being stopped" "$(cat "$test_tmp/output")" +pass "a gateway that was running is running again from the runtime, or the install fails saying it is stopped" + +# With the runtime already in place nothing is seeded, so the move is Omarchy's. +new_home runtime-first +run omarchy-install-openclaw-cli --now || fail "--now sets OpenClaw up" "$(cat "$test_tmp/output")" +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +touch "$test_home/active-openclaw-gateway.service" +: >"$events" +run omarchy-install-openclaw-cli --now || fail "--now moves a gateway beside a runtime that already runs" "$(cat "$test_tmp/output")" +! grep -q '^install-cli' "$events" && grep -Fxq "runtime gateway install --force --runtime-path $test_home/.openclaw/tools/node-v24.19.0/bin/node" "$events" && [[ -e $test_home/active-openclaw-gateway.service ]] || + fail "--now moves a gateway beside a runtime that already runs" "$(cat "$events")" +pass "a gateway beside a runtime that already runs is moved without seeding" + +# A move that stopped halfway left the runtime's code on the old package's +# /usr/bin/node; the next run finishes it, and moves a node host the same way. +new_home half-moved +run omarchy-install-openclaw-cli --now || fail "--now sets OpenClaw up" "$(cat "$test_tmp/output")" +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node %s/.openclaw/tools/node-v24.19.0/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' "$test_home" >"$test_home/.config/systemd/user/openclaw-gateway.service" +printf 'ExecStart=/usr/bin/node %s/.openclaw/tools/node-v24.19.0/lib/node_modules/openclaw/dist/index.js node run\n' "$test_home" >"$test_home/.config/systemd/user/openclaw-node.service" +touch "$test_home/active-openclaw-gateway.service" "$test_home/active-openclaw-node.service" +: >"$events" +run omarchy-install-openclaw-cli --now || fail "--now finishes a half-moved gateway" "$(cat "$test_tmp/output")" +for role in gateway node; do + grep -Fxq "runtime $role install --force --runtime-path $test_home/.openclaw/tools/node-v24.19.0/bin/node" "$events" && + sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$test_home/.config/systemd/user/openclaw-$role.service" | grep -qx "$test_home/.openclaw/tools/node-v24.19.0/bin/node" || + fail "--now finishes a half-moved $role on the runtime's own Node" "$(cat "$events")" +done +pass "a half-moved gateway and a node host end up on the runtime's own Node" + +# The runtime run by any other Node or by Bun is the user's choice, not the old +# package's dependency. +new_home bun +run omarchy-install-openclaw-cli --now || fail "--now sets OpenClaw up" "$(cat "$test_tmp/output")" +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/bun %s/.openclaw/tools/node-v24.19.0/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' "$test_home" >"$test_home/.config/systemd/user/openclaw-gateway.service" +cp "$test_home/.config/systemd/user/openclaw-gateway.service" "$test_tmp/bun-unit" +: >"$events" +run omarchy-install-openclaw-cli --now || fail "--now leaves a Bun gateway alone" "$(cat "$test_tmp/output")" +! grep -q "install --force\|^systemctl" "$events" && cmp -s "$test_tmp/bun-unit" "$test_home/.config/systemd/user/openclaw-gateway.service" || + fail "--now leaves a Bun gateway alone" "$(cat "$events")" +printf 'ExecStart=/usr/bin/node %s/.openclaw/custom/bridge.js\n' "$test_home" >"$test_home/.config/systemd/user/openclaw-gateway.service" +cp "$test_home/.config/systemd/user/openclaw-gateway.service" "$test_tmp/own-unit" +: >"$events" +run omarchy-install-openclaw-cli --now || fail "--now leaves a script of the user's alone" "$(cat "$test_tmp/output")" +! grep -q "install --force\|^systemctl" "$events" && cmp -s "$test_tmp/own-unit" "$test_home/.config/systemd/user/openclaw-gateway.service" || + fail "--now leaves a script of the user's alone" "$(cat "$events")" +pass "a gateway the user runs on Bun, or a script of their own on /usr/bin/node, is left alone" + +# The migration moves only machines that have the package, and waits for the +# package that seeds. +new_home migration-none +run bash -euo pipefail "$test_tmp/migration.sh" || fail "a machine without OpenClaw has nothing to move" "$(cat "$test_tmp/output")" +[[ ! -s $events && ! -e $test_home/.openclaw ]] || fail "a machine without OpenClaw is untouched" "$(cat "$events")" + +new_home migration-old +touch "$test_tmp/package-installed" +mv "$seed" "$seed.old" +run bash -euo pipefail "$test_tmp/migration.sh" && fail "an old package keeps the migration pending" +[[ ! -e $test_home/.openclaw ]] || fail "an old package leaves the home untouched" +mv "$seed.old" "$seed" + +new_home migration +touch "$test_tmp/package-installed" +mkdir -p "$test_home/.config/systemd/user" +printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$test_home/.config/systemd/user/openclaw-gateway.service" +run bash -euo pipefail "$test_tmp/migration.sh" || fail "the migration moves OpenClaw" "$(cat "$test_tmp/output")" +grep -q "^install-cli " "$events" && grep -Fxq "runtime gateway install --force" "$events" || + fail "the migration seeds the runtime and moves the gateway to it" "$(cat "$events")" +[[ $(readlink -- "$command") == "$runtime" ]] || fail "the migration points the command at the runtime" +sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$test_home/.config/systemd/user/openclaw-gateway.service" | grep -qx "$test_home/.openclaw/tools/node-v24.19.0/bin/node" || + fail "the migration leaves the gateway on the runtime's own Node, not the old package's system Node" "$(cat "$test_home/.config/systemd/user/openclaw-gateway.service")" + +new_home migration-foreign +touch "$test_tmp/package-installed" +printf '#!/bin/bash\n' >"$command" +run bash -euo pipefail "$test_tmp/migration.sh" && fail "a migration that cannot finish stays pending" +pass "the migration moves a packaged OpenClaw to its runtime, and stays pending until it can" diff --git a/test/shell.d/openclaw-onboard-test.sh b/test/shell.d/openclaw-onboard-test.sh index 852e3e3d..1c315fdd 100755 --- a/test/shell.d/openclaw-onboard-test.sh +++ b/test/shell.d/openclaw-onboard-test.sh @@ -55,6 +55,7 @@ onboard) while :; do sleep 0.2; done ;; dashboard) + printf 'dashboard-stdin:%s\n' "$(readlink /proc/$$/fd/0)" >>"$TEST_LOG" [[ -f $HOME/.openclaw/openclaw.json ]] && echo '{"ok":true,"port":18789}' || { echo '{"ok":false}'; exit 1; } ;; esac @@ -63,7 +64,10 @@ chmod +x "$tmp_dir/bin/openclaw" start=$SECONDS rc=0 -"$ROOT/bin/omarchy-openclaw-onboard" /dev/null 2>&1 || rc=$? +# A file stands in for the terminal the wizard reads, so where each probe's +# stdin points can be told apart from it. +: >"$tmp_dir/terminal" +"$ROOT/bin/omarchy-openclaw-onboard" <"$tmp_dir/terminal" >/dev/null 2>&1 || rc=$? elapsed=$((SECONDS - start)) grep -q '^openclaw:onboard --flow quickstart --install-daemon --skip-ui$' "$TEST_LOG" || @@ -76,6 +80,14 @@ grep -q '^terminated$' "$TEST_LOG" || (( elapsed < 30 )) || fail "a wizard that lingers after the gateway is up is stopped and counts as success" "took ${elapsed}s" pass "a wizard that lingers after the gateway is up is stopped and counts as success" +# OpenClaw's CLI takes a terminal on stdin out of raw mode as the timeout kills +# it, so a probe that inherited the wizard's terminal would leave its prompts +# unable to read keys. The wizard writes its config while prompts remain. +grep -q '^dashboard-stdin:' "$TEST_LOG" || fail "gateway probes never read from the wizard's terminal" "no probe ran" +! grep '^dashboard-stdin:' "$TEST_LOG" | grep -vqx 'dashboard-stdin:/dev/null' || + fail "gateway probes never read from the wizard's terminal" "$(grep '^dashboard-stdin:' "$TEST_LOG" | sort -u)" +pass "gateway probes never read from the wizard's terminal" + # The wizard only starts being stopped once the gateway actually answers: a # stub that never writes the config is left alone and must be ended by its own # exit, not the watcher. diff --git a/test/shell.d/remove-ai-test.sh b/test/shell.d/remove-ai-test.sh index d81e6ca6..03887c53 100644 --- a/test/shell.d/remove-ai-test.sh +++ b/test/shell.d/remove-ai-test.sh @@ -245,6 +245,8 @@ fresh_openclaw_home() { "$HOME/.local/share/applications" "$HOME/.local/share/icons/hicolor/256x256/apps" touch "$HOME/.config/systemd/user/openclaw-gateway.service" \ "$HOME/.config/systemd/user/openclaw-gateway.service.bak" \ + "$HOME/.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.bak" \ + "$HOME/.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.receipt.bak" \ "$HOME/.config/systemd/user/openclaw-node.service" \ "$HOME/.openclaw/openclaw.json" \ "$HOME/.local/share/applications/OpenClaw.desktop" \ @@ -269,6 +271,8 @@ fresh_openclaw_home for gone in .config/systemd/user/openclaw-gateway.service \ .config/systemd/user/openclaw-gateway.service.bak \ + .config/systemd/user/openclaw-gateway.service.reconcile-0f1e.bak \ + .config/systemd/user/openclaw-gateway.service.reconcile-0f1e.receipt.bak \ .config/systemd/user/default.target.wants/openclaw-gateway.service \ .config/systemd/user/openclaw-node.service \ .config/systemd/user/default.target.wants/openclaw-node.service \ @@ -295,6 +299,24 @@ pass "OpenClaw removal stops the gateway service" fail "OpenClaw removal keeps the user's agent state" "asked about ~/.openclaw without a terminal" pass "OpenClaw removal keeps the user's agent state" +# The command on PATH goes; the OpenClaw it pointed at stays with the state, +# since only that copy is sure to open it. A link somewhere else is not Omarchy's. +fresh_openclaw_home +mkdir -p "$HOME/.openclaw/bin" "$HOME/.openclaw/tools/node-v24.19.0/lib" "$HOME/.local/bin" +printf '#!/usr/bin/env bash\n' >"$HOME/.openclaw/bin/openclaw" +ln -s "$HOME/.openclaw/bin/openclaw" "$HOME/.local/bin/openclaw" +"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null +[[ ! -e $HOME/.local/bin/openclaw && ! -L $HOME/.local/bin/openclaw ]] || fail "OpenClaw removal takes openclaw off PATH" +[[ -f $HOME/.openclaw/bin/openclaw && -d $HOME/.openclaw/tools && -f $HOME/.openclaw/openclaw.json ]] || + fail "OpenClaw removal keeps the OpenClaw that can open the state" + +fresh_openclaw_home +mkdir -p "$HOME/.local/bin" +ln -s /opt/openclaw/openclaw "$HOME/.local/bin/openclaw" +"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null +[[ -L $HOME/.local/bin/openclaw ]] || fail "OpenClaw removal takes openclaw off PATH" "someone else's link went with it" +pass "OpenClaw removal takes openclaw off PATH and keeps the copy that can open the state" + # A CLI that knows `gateway uninstall` owns the teardown; the manual systemd # fallback must not run. cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'