Files
omarchy/test/shell.d/remove-ai-test.sh
T
Spencer Bull 5345673988 Add OpenClaw to Install > AI as a web app on its own gateway
OpenClaw's desktop experience on Linux is its Control UI, served by the
gateway the openclaw package runs, so the Install > AI entry installs
the package and a web app launcher that routes through the new
omarchy-launch-openclaw: first launch hands off to OpenClaw's own
onboarding wizard, later launches start the gateway when needed and open
the dashboard's single-use browser handoff URL as an app window.
Remove > AI tears the gateway service down through OpenClaw's own
gateway uninstall (falling back to systemctl by hand), aborts rather
than dropping the package under a gateway that will not stop, and keeps
the user's agent in ~/.openclaw.

OpenClaw also joins Setup > Defaults > Agent through the same
agent_installer seam Hermes carries: its CLI is the pacman package
rather than a mise tool, so omarchy-install-openclaw-cli answers
--check/--now with pacman, and omarchy-agent runs `openclaw chat`,
seeding prompts through --message.

The menu mark is a new U+E90C glyph traced from the package's lobster
favicon; E90B stays free for the Perplexity mark still in flight on its
own branch.
The launcher recovers the gateway through `openclaw gateway install --force`
(unit not enabled: missing, or an install that died after writing it) or
`openclaw gateway start` (enabled but stopped), never `openclaw dashboard
--yes`: as of OpenClaw 2026.9.1 that defers to "the owning supervisor" in
both cases, and once the gateway is up it copies a one-time browser pairing
URL into the clipboard. The dashboard probe is bounded so an app-grid launch
cannot hang without a terminal to interrupt it. Removal treats only
systemd's own "inactive"/"failed" as a stopped gateway, so an unreachable
user manager aborts instead of dropping the package under a live process.
All of it verified against a real 2026.9.1 install.
Removal also takes down the node-host unit if OpenClaw ever installed one, and
asks (default no, only on a terminal) whether ~/.openclaw should go too, with
its size: the chats and credentials live there next to hundreds of megabytes
of plugin runtimes and cache OpenClaw downloads for itself.
Onboarding goes through omarchy-openclaw-onboard rather than bare `openclaw
onboard`: as of 2026.9.1 the bare command is the guided flow, which ends by
running a foreground gateway and handing off to a browser tab without
returning, so the install script never reached the app launch and no service
was installed. The helper runs the classic wizard (--flow quickstart
--install-daemon --skip-ui) as a background job that keeps the terminal as its
stdin, so its prompts render and take input as upstream draws them, and stops
it once the gateway answers: upstream leaves the wizard running after its
outro (only the TUI branch exits, and the model sign-in holds a socket open).
Every quickstart prompt precedes the service install, so that point is safe.
A gateway that never comes up after this run applies setup ends the wait as a
failure instead
of hanging, an already-running OpenClaw is left alone rather than mistaken for
this run's success, a gateway answering on the port is only this run's once its
process is the unit's own MainPID (an orphan from an
earlier run) is not mistaken for the service this run installs, and a signal at
the helper takes the wizard down with it.
2026-09-05 09:18:27 -05:00

254 lines
10 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin"
cat >"$tmp_dir/bin/omarchy-pkg-drop" <<'SCRIPT'
#!/bin/bash
printf 'drop:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/omarchy-pkg-drop"
export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir/bin:$PATH"
fresh_home() {
rm -rf "$tmp_dir/home"
mkdir -p "$tmp_dir/home"
export HOME="$tmp_dir/home"
}
# The Codex CLI ships in its own package and resolves its runtime out of
# ~/.cache/codex-runtimes, so removing the desktop app must not take it.
fresh_home
mkdir -p "$HOME/.config/Codex" "$HOME/.cache/Codex" "$HOME/.cache/codex-runtimes/codex-primary-runtime" "$HOME/.codex"
"$ROOT/bin/omarchy-remove-ai-chatgpt" >/dev/null
[[ ! -e $HOME/.config/Codex ]] || fail "ChatGPT removal deletes the desktop app's config"
pass "ChatGPT removal deletes the desktop app's config"
[[ -d $HOME/.cache/codex-runtimes/codex-primary-runtime ]] || fail "ChatGPT removal keeps the Codex CLI's runtime cache"
pass "ChatGPT removal keeps the Codex CLI's runtime cache"
[[ -d $HOME/.codex ]] || fail "ChatGPT removal keeps the Codex CLI's config"
pass "ChatGPT removal keeps the Codex CLI's config"
# LM Studio's models follow a relocatable home, named only by the pointer file.
fresh_home
mkdir -p "$tmp_dir/relocated-models/models"
printf '%s' "$tmp_dir/relocated-models" >"$HOME/.lmstudio-home-pointer"
mkdir -p "$HOME/.config/LM Studio"
"$ROOT/bin/omarchy-remove-ai-lm-studio" >/dev/null
[[ ! -e $tmp_dir/relocated-models ]] || fail "LM Studio removal follows a relocated home pointer"
pass "LM Studio removal follows a relocated home pointer"
[[ ! -e "$HOME/.config/LM Studio" ]] || fail "LM Studio removal deletes its config"
pass "LM Studio removal deletes its config"
# A pointer that resolves to the home directory itself would take everything.
fresh_home
printf '%s' "$HOME" >"$HOME/.lmstudio-home-pointer"
mkdir -p "$HOME/Documents"
"$ROOT/bin/omarchy-remove-ai-lm-studio" >/dev/null
[[ -d $HOME/Documents ]] || fail "LM Studio removal refuses a pointer aimed at the home directory"
pass "LM Studio removal refuses a pointer aimed at the home directory"
# T3 Code bootstraps the agents it drives; their state outlives it.
fresh_home
mkdir -p "$HOME/.config/t3code" "$HOME/.t3" "$HOME/.grok" "$HOME/.local/share/opencode" "$HOME/.npm"
touch "$HOME/.claude.json"
"$ROOT/bin/omarchy-remove-ai-t3-code" >/dev/null
[[ ! -e $HOME/.t3 ]] || fail "T3 Code removal deletes its own data"
pass "T3 Code removal deletes its own data"
for kept in .grok .claude.json .npm .local/share/opencode; do
[[ -e $HOME/$kept ]] || fail "T3 Code removal keeps the agent state it bootstrapped" "$kept"
done
pass "T3 Code removal keeps the agent state it bootstrapped"
# ~/.grok belongs to the Grok CLI that omarchy-default-agent installs.
fresh_home
mkdir -p "$HOME/.config/Grok Bot" "$HOME/.grokbot" "$HOME/.grok"
"$ROOT/bin/omarchy-remove-ai-grok-bot" >/dev/null
[[ ! -e $HOME/.grokbot ]] || fail "Grok Bot removal deletes its own data"
pass "Grok Bot removal deletes its own data"
[[ -d $HOME/.grok ]] || fail "Grok Bot removal keeps the Grok CLI's state"
pass "Grok Bot removal keeps the Grok CLI's state"
# Every acceleration variant depends on the base package, so package presence is
# the test the remover can actually act on; the command alone is also provided by
# builds omarchy-pkg-drop will not touch.
ollama_row=$(grep '^ "remove.ai.ollama":' "$ROOT/default/omarchy/omarchy-menu.jsonc")
[[ $ollama_row == *'"when":"omarchy-pkg-present ollama"'* ]] ||
fail "Ollama removal is offered only where the package is installed" "$ollama_row"
pass "Ollama removal is offered only where the package is installed"
# OpenClaw's gateway unit and web app launcher are the app's own; the agent
# state in ~/.openclaw is the user's. systemctl and openclaw are stubbed so the
# sandbox never reaches the real user manager or a real gateway.
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
# Default openclaw stub: the packaged CLI predates `gateway uninstall`, so the
# remover has to fall back to its manual systemd path.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
fresh_openclaw_home() {
fresh_home
mkdir -p "$HOME/.config/systemd/user/default.target.wants" "$HOME/.openclaw" \
"$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-node.service" \
"$HOME/.openclaw/openclaw.json" \
"$HOME/.local/share/applications/OpenClaw.desktop" \
"$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
ln -s ../openclaw-gateway.service \
"$HOME/.config/systemd/user/default.target.wants/openclaw-gateway.service"
ln -s ../openclaw-node.service \
"$HOME/.config/systemd/user/default.target.wants/openclaw-node.service"
}
# gum would ask about ~/.openclaw; the tests never run on a terminal, so it
# must not even be reached. Logging it proves that.
cat >"$tmp_dir/bin/gum" <<'SCRIPT'
#!/bin/bash
printf 'gum:%s\n' "$*" >>"$TEST_LOG"
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/gum"
fresh_openclaw_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
for gone in .config/systemd/user/openclaw-gateway.service \
.config/systemd/user/openclaw-gateway.service.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 \
.local/share/applications/OpenClaw.desktop \
.local/share/icons/hicolor/256x256/apps/openclaw.png; do
[[ ! -e $HOME/$gone && ! -L $HOME/$gone ]] || fail "OpenClaw removal deletes the service and launcher it installed" "$gone"
done
pass "OpenClaw removal deletes the service and launcher it installed"
grep -q '^systemctl:--user disable --now openclaw-gateway.service$' "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service"
grep -q '^systemctl:--user disable --now openclaw-node.service$' "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service" "node host service left running"
for unit in openclaw-gateway.service openclaw-node.service; do
grep -q "^systemctl:--user reset-failed $unit\$" "$TEST_LOG" ||
fail "OpenClaw removal stops the gateway service" "$unit left in systemd's failed list"
done
pass "OpenClaw removal stops the gateway service"
# Without a terminal there is nobody to ask, so the state stays and gum is
# never invoked (a gum that answered "yes" on its own would be a data loss).
[[ -f $HOME/.openclaw/openclaw.json ]] || fail "OpenClaw removal keeps the user's agent state"
! grep -q '^gum:' "$TEST_LOG" ||
fail "OpenClaw removal keeps the user's agent state" "asked about ~/.openclaw without a terminal"
pass "OpenClaw removal keeps the user's agent state"
# A CLI that knows `gateway uninstall` owns the teardown; the manual systemd
# fallback must not run.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
rm -f "$HOME/.config/systemd/user/openclaw-$1.service" \
"$HOME/.config/systemd/user/default.target.wants/openclaw-$1.service"
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
: >"$TEST_LOG"
fresh_openclaw_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
grep -q '^openclaw:gateway uninstall$' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown"
grep -q '^openclaw:node uninstall$' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown" "node host not handed to upstream"
! grep -q '^systemctl:--user disable' "$TEST_LOG" ||
fail "OpenClaw removal prefers upstream's own gateway teardown" "manual disable ran too"
pass "OpenClaw removal prefers upstream's own gateway teardown"
# Without the unit file there is nothing of ours registered, so systemd stays untouched.
systemctl_calls_before=$(grep -c '^systemctl:' "$TEST_LOG" || true)
fresh_home
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null
systemctl_calls_after=$(grep -c '^systemctl:' "$TEST_LOG" || true)
[[ $systemctl_calls_before == "$systemctl_calls_after" ]] ||
fail "OpenClaw removal leaves systemd alone when onboarding never ran"
pass "OpenClaw removal leaves systemd alone when onboarding never ran"
# A gateway that will not stop aborts the removal before the package drop:
# pacman would otherwise strand the live process on deleted code.
cat >"$tmp_dir/bin/openclaw" <<'SCRIPT'
#!/bin/bash
printf 'openclaw:%s\n' "$*" >>"$TEST_LOG"
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/openclaw"
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
[[ $2 == disable ]] && exit 1
[[ $2 == is-active ]] && exit 0
exit 0
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
drop_calls_before=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
fresh_openclaw_home
rc=0
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
drop_calls_after=$(grep -c '^drop:openclaw$' "$TEST_LOG" || true)
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when the gateway cannot be stopped"
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "unit file deleted"
[[ $drop_calls_before == "$drop_calls_after" ]] ||
fail "OpenClaw removal aborts when the gateway cannot be stopped" "package dropped anyway"
pass "OpenClaw removal aborts when the gateway cannot be stopped"
# An unreachable user manager is not a stopped gateway: every probe failing
# with no answer must still abort, not read as "confirmed inactive".
cat >"$tmp_dir/bin/systemctl" <<'SCRIPT'
#!/bin/bash
printf 'systemctl:%s\n' "$*" >>"$TEST_LOG"
echo "Failed to connect to user scope bus" >&2
exit 1
SCRIPT
chmod +x "$tmp_dir/bin/systemctl"
: >"$TEST_LOG"
fresh_openclaw_home
rc=0
"$ROOT/bin/omarchy-remove-ai-openclaw" >/dev/null 2>&1 || rc=$?
[[ $rc != 0 ]] || fail "OpenClaw removal aborts when systemd cannot be reached"
[[ -f $HOME/.config/systemd/user/openclaw-gateway.service ]] ||
fail "OpenClaw removal aborts when systemd cannot be reached" "unit file deleted"
! grep -q '^drop:openclaw$' "$TEST_LOG" ||
fail "OpenClaw removal aborts when systemd cannot be reached" "package dropped anyway"
pass "OpenClaw removal aborts when systemd cannot be reached"