Files
omarchy/test/shell.d/agent-account-test.sh
T
David Heinemeier HanssonandClaude Opus 5.5 f45461a38f Bring Grok up to par with Claude and Codex in the agents panel (#13992)
* Let Grok updates through mise take effect

Grok's npm launcher runs ~/.grok/bin/grok whenever it exists, and the
install script that repoints it at a new release doesn't run under mise,
so every machine kept running the release it first unpacked. Updating
mise tools now drops a link to an older release and the old binary, and
lets the launcher unpack the installed one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep several Grok accounts, the way Claude and Codex do

Grok honors GROK_HOME, so an added Grok account gets its own home holding
only its login and settings cache, with the CLI binary, sessions, skills,
plugins, memory, and config linked back to ~/.grok. The account commands,
the add flow (a second account signs in through a private window), the
launcher, and a grok shell function all take it like the others, and its
identity and plan come from the files Grok writes at login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show Grok's plan and credits in the agents panel

A grok collector reads each account's plan from the settings Grok caches
and its credits from the endpoint behind Grok's own /usage view, so Grok
gets a section, per-account limits, and autoswitch like Claude and Codex.
A signed-in agent with a plan now shows before its first numbers, so a
lapsed sign-in has somewhere to say so. Grok's mark joins the assets, and
with every agent able to take another account the add screen no longer
needs to dim one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document Grok accounts and limits alongside Claude and Codex

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read Grok's credits the way xAI actually answers

The answer nests under config, names a weekly period with its end, and
as protobuf JSON leaves the usage percentage out while it's zero. The
collector now reads that shape, so a fresh week shows as 0% until it
resets rather than as nothing known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count Grok's prompts and sessions from its session summaries

Grok keeps a summary beside each session with when it was last active
and how many prompts it had, so its section and the hero's summary get
today's prompts and sessions and its active days. It records no token
counts there, so none are claimed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Give each Grok limits cache write its own temporary file

Two overlapping collector runs wrote the same cache through one .tmp
path, so one rename could leave the other's failing and its record not
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read a single Grok account from GROK_HOME when it's set

The launcher and the CLI honor GROK_HOME, but the collector always read
~/.grok, so a Grok signed in only in a custom home showed nothing. With
one account, the collector now reads the home the CLI would; with
several registered, the primary stays ~/.grok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop guesses the real Grok credits answer made unnecessary

The collector carried fallbacks for fields and shapes guessed from the
binary before xAI's actual answer was seen, a numeric timestamp branch
nothing writes, the panel's default for prompt stats, and guards that an
empty sign-in already covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the working Grok release until its update is in place

The update dropped the link and older binaries before the new release was
unpacked and ignored a failed unpack, which could leave every Grok
account without a CLI. The old release now stays until the new one is
linked, and its link comes back if it never is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count only Grok sessions for today, not prompts

A session's summary holds all its prompts and only when it was last
active, so a resumed session put its whole history on today. Today now
counts the sessions active in it; prompts stay an all-time total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reuse the Grok session scan on a limits-only refresh

Opening the panel and near-limit checks only need fresh limits, so they
reuse a scan up to 15 minutes old, as the Codex collector does; --force
still rescans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:37:56 +02:00

421 lines
23 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command jq
require_command python3
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
notifications="$test_tmp/notifications"
mkdir -p "$mock_bin" "$test_tmp/home/.claude" "$test_tmp/home/.codex"
cat >"$mock_bin/omarchy-notification-send" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_NOTIFICATIONS"
SH
# The CLIs report which home they were started in, and a login writes the
# identity a real one would into whichever home it was given.
cat >"$mock_bin/claude" <<'SH'
#!/bin/bash
if [[ ${1:-} == "auth" && ${2:-} == "login" ]]; then
"${BROWSER:-omarchy-test-default-browser}" "https://claude.com/oauth/authorize"
if [[ -n ${OMARCHY_TEST_LOGIN_HANGS:-} ]]; then
echo $$ >"$OMARCHY_TEST_LOGIN_HANGS"
exec sleep 30
fi
[[ -n ${OMARCHY_TEST_LOGIN_UUID:-} ]] || exit 1
home=${CLAUDE_CONFIG_DIR:-$HOME/.claude}
account_file="$home/.claude.json"
[[ -z ${CLAUDE_CONFIG_DIR:-} ]] && account_file="$HOME/.claude.json"
printf '{"oauthAccount":{"accountUuid":"%s","emailAddress":"%s","organizationName":"Work"}}\n' \
"$OMARCHY_TEST_LOGIN_UUID" "$OMARCHY_TEST_LOGIN_EMAIL" >"$account_file"
echo '{"claudeAiOauth":{"rateLimitTier":"default_claude_max_5x","subscriptionType":"max"}}' >"$home/.credentials.json"
exit 0
fi
echo "claude home=${CLAUDE_CONFIG_DIR:-default} args=$*"
SH
cat >"$mock_bin/codex" <<'SH'
#!/bin/bash
if [[ ${1:-} == "login" ]]; then
"${BROWSER:-omarchy-test-default-browser}" "https://auth.openai.com/oauth/authorize"
claims=$(printf '{"email":"%s","https://api.openai.com/auth":{"chatgpt_plan_type":"pro"}}' "$OMARCHY_TEST_LOGIN_EMAIL" | base64 -w0 | tr '+/' '-_' | tr -d '=')
printf '{"auth_mode":"chatgpt","tokens":{"account_id":"%s","id_token":"h.%s.s"}}\n' "$OMARCHY_TEST_LOGIN_UUID" "$claims" >"${CODEX_HOME:-$HOME/.codex}/auth.json"
exit 0
fi
echo "codex home=${CODEX_HOME:-default} args=$*"
SH
# Grok keys its login by issuer and keeps the plan in the settings it caches,
# as a JSON string inside that file.
cat >"$mock_bin/grok" <<'SH'
#!/bin/bash
if [[ ${1:-} == "login" ]]; then
"${BROWSER:-omarchy-test-default-browser}" "https://auth.x.ai/oauth/authorize"
home=${GROK_HOME:-$HOME/.grok}
mkdir -p "$home"
printf '{"https://auth.x.ai::client":{"key":"t","user_id":"%s","email":"%s"}}\n' \
"${OMARCHY_TEST_LOGIN_UUID:-u-grok}" "${OMARCHY_TEST_LOGIN_EMAIL:-me@example.com}" >"$home/auth.json"
printf '{"payload":"{\\"settings\\":{\\"subscription_tier_display\\":\\"SuperGrok\\"}}"}\n' >"$home/settings_cache.json"
exit 0
fi
echo "grok home=${GROK_HOME:-default} args=$*"
SH
cat >"$mock_bin/omarchy-test-default-browser" <<'SH'
#!/bin/bash
printf 'default %s\n' "$*" >>"$OMARCHY_TEST_BROWSER_LOG"
SH
cat >"$mock_bin/omarchy-agent-usage-update" <<'SH'
#!/bin/bash
SH
cat >"$mock_bin/omarchy-default-agent" <<'SH'
#!/bin/bash
echo "${OMARCHY_TEST_DEFAULT_AGENT-claude}"
SH
cat >"$mock_bin/omarchy-launch-browser" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_BROWSER_LOG"
SH
chmod +x "$mock_bin"/*
export HOME="$test_tmp/home"
export XDG_STATE_HOME="$test_tmp/state"
export PATH="$mock_bin:$ROOT/bin:$PATH"
export OMARCHY_PATH="$ROOT"
export OMARCHY_TEST_NOTIFICATIONS="$notifications"
export OMARCHY_TEST_BROWSER_LOG="$test_tmp/browser"
unset CLAUDE_CONFIG_DIR CODEX_HOME GROK_HOME BROWSER
accounts="$XDG_STATE_HOME/omarchy/agents/accounts"
echo '{"oauthAccount":{"accountUuid":"u-main","emailAddress":"me@example.com","organizationName":"Me"},"mcpServers":{"docs":{"command":"docs-mcp"}}}' >"$HOME/.claude.json"
echo '{"claudeAiOauth":{"rateLimitTier":"default_claude_max_20x","subscriptionType":"max"}}' >"$HOME/.claude/.credentials.json"
echo '{"theme":"custom:omarchy"}' >"$HOME/.claude/settings.json"
# ---------------------------------------------------------------- single account
[[ -z $(omarchy-agent-account-home claude) ]] || fail "a machine with one account routes nowhere"
[[ ! -e $accounts/claude.json ]] || fail "reading the active home never creates a registry"
pass "a machine with one account launches exactly as before"
list=$(omarchy-agent-account-list claude --json)
[[ $(jq -c '.[0].accounts | map({id, label, plan, email, active, primary})' <<<"$list") == '[{"id":"main","label":"Main","plan":"Max 20x","email":"me@example.com","active":true,"primary":true}]' ]] ||
fail "the existing login is listed as the primary account" "$list"
pass "the existing login is listed as the primary account"
# ------------------------------------------------------------------------- add
OMARCHY_TEST_LOGIN_UUID=u-work OMARCHY_TEST_LOGIN_EMAIL=work@example.com \
omarchy-agent-account-add claude Work </dev/null >"$test_tmp/add-output"
grep -q "Added Work (work@example.com)" "$test_tmp/add-output" || fail "adding an account reports who signed in" "$(cat "$test_tmp/add-output")"
grep -q "private window that opens" "$test_tmp/add-output" || fail "adding an account says to sign in within the private window"
[[ $(head -1 "$OMARCHY_TEST_BROWSER_LOG") == "--private https://claude.com/oauth/authorize" ]] ||
fail "a Claude login opens in a private window" "$(cat "$OMARCHY_TEST_BROWSER_LOG")"
pass "adding an account signs in through the CLI's own login"
work="$accounts/claude/work"
[[ -f $work/.credentials.json && ! -L $work/.credentials.json ]] || fail "an added account keeps its own credentials"
[[ $(readlink "$work/projects") == "$HOME/.claude/projects" ]] || fail "an added account shares conversation history with the primary"
[[ $(readlink "$work/settings.json") == "$HOME/.claude/settings.json" ]] || fail "an added account shares settings with the primary"
[[ $(readlink "$work/CLAUDE.md") == "$HOME/.claude/CLAUDE.md" && ! -e $HOME/.claude/CLAUDE.md ]] ||
fail "a shared file the primary doesn't have yet is linked, so it's written there when it is"
[[ $(jq -c .mcpServers "$work/.claude.json") == '{"docs":{"command":"docs-mcp"}}' ]] || fail "an added account carries the primary's MCP servers"
[[ $(stat -c %a "$work") == 700 && $(stat -c %a "$accounts/claude.json") == 600 ]] || fail "account homes and the registry are private"
pass "an added account shares everything but its login with the primary"
if OMARCHY_TEST_LOGIN_UUID=u-work OMARCHY_TEST_LOGIN_EMAIL=work@example.com \
omarchy-agent-account-add claude Again </dev/null >"$test_tmp/dup-output" 2>&1; then
fail "adding the same account twice fails"
fi
grep -q "That's Work" "$test_tmp/dup-output" || fail "a duplicate login names the account it already is" "$(cat "$test_tmp/dup-output")"
[[ -z $(ls -A "$accounts/claude/.pending") ]] || fail "a duplicate login leaves no scratch home behind"
pass "adding an account that's already there is refused"
# Main was signed in to another account by hand since the registry was made;
# adding that login again is still the same subscription.
jq '.oauthAccount.accountUuid = "u-relogged"' "$HOME/.claude.json" >"$test_tmp/relogged.json"
cp "$HOME/.claude.json" "$test_tmp/original.json"
mv "$test_tmp/relogged.json" "$HOME/.claude.json"
if OMARCHY_TEST_LOGIN_UUID=u-relogged OMARCHY_TEST_LOGIN_EMAIL=me@example.com \
omarchy-agent-account-add claude Twin </dev/null >"$test_tmp/twin-output" 2>&1; then
fail "a login matching Main's current sign-in is refused"
fi
grep -q "That's Main" "$test_tmp/twin-output" || fail "a login matching Main's current sign-in names Main" "$(cat "$test_tmp/twin-output")"
mv "$test_tmp/original.json" "$HOME/.claude.json"
pass "duplicates are judged by who each home is signed in as now"
if OMARCHY_TEST_LOGIN_UUID="" omarchy-agent-account-add claude Nope </dev/null >/dev/null 2>&1; then
fail "an abandoned login adds nothing"
fi
[[ $(omarchy-agent-account-list claude --json | jq '.[0].accounts | length') == 2 ]] || fail "an abandoned login adds nothing"
pass "an abandoned login adds nothing"
OMARCHY_TEST_LOGIN_UUID=u-next OMARCHY_TEST_LOGIN_EMAIL=next@example.com \
omarchy-agent-account-add claude Next </dev/null >/dev/null
[[ $(omarchy-agent-account-list claude --json | jq -r '.[0].accounts[] | select(.label == "Next") | .id') == "next-2" ]] ||
fail "an account label can't take an id that routing already answers to"
omarchy-agent-account-remove claude next-2 </dev/null >/dev/null
pass "account ids stay clear of routing keywords"
OMARCHY_TEST_LOGIN_UUID=acct-1 OMARCHY_TEST_LOGIN_EMAIL=me@example.com \
omarchy-agent-account-add codex </dev/null >"$test_tmp/first-codex"
[[ -f $HOME/.codex/auth.json && ! -d $accounts/codex ]] || fail "the first Codex account signs in to ~/.codex itself"
grep -qx "default https://auth.openai.com/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
fail "the first Codex account signs in through the normal browser" "$(cat "$OMARCHY_TEST_BROWSER_LOG")"
[[ ! -e $HOME/.config/omarchy/defaults/agent ]] || fail "a first sign-in leaves an existing default agent alone"
pass "the first account of a provider signs in to its own home in the normal browser"
OMARCHY_TEST_LOGIN_UUID=acct-2 OMARCHY_TEST_LOGIN_EMAIL=side@example.com \
omarchy-agent-account-add codex Side </dev/null >/dev/null
[[ $(omarchy-agent-account-list codex --json | jq -c '.[0].accounts[1] | {id, email, plan}') == '{"id":"side","email":"side@example.com","plan":"Pro"}' ]] ||
fail "a Codex account reads its identity from the login's token claims"
[[ $(readlink "$accounts/codex/side/sessions") == "$HOME/.codex/sessions" ]] || fail "a Codex account shares sessions with the primary"
grep -qx -- "--private https://auth.openai.com/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
fail "a Codex login opens in a private window" "$(cat "$OMARCHY_TEST_BROWSER_LOG")"
pass "Codex accounts are added the same way"
OMARCHY_TEST_DEFAULT_AGENT="" omarchy-agent-account-add grok </dev/null >/dev/null
[[ $(cat "$HOME/.config/omarchy/defaults/agent") == "grok" ]] ||
fail "the first agent signed in on a machine with no default becomes the default"
[[ -s $HOME/.grok/auth.json ]] && grep -qx "default https://auth.x.ai/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
fail "a first Grok sign-in lands in ~/.grok through the normal browser"
pass "Grok signs in its first account"
mkdir -p "$HOME/.grok/bin" && touch "$HOME/.grok/bin/grok"
OMARCHY_TEST_LOGIN_UUID=u-grok-2 OMARCHY_TEST_LOGIN_EMAIL=side@example.com \
omarchy-agent-account-add grok Side </dev/null >/dev/null
[[ $(omarchy-agent-account-list grok --json | jq -c '.[0].accounts[1] | {id, email, plan}') == '{"id":"side","email":"side@example.com","plan":"SuperGrok"}' ]] ||
fail "a Grok account reads its identity and plan from its own home" "$(omarchy-agent-account-list grok --json)"
[[ $(readlink "$accounts/grok/side/bin") == "$HOME/.grok/bin" && $(readlink "$accounts/grok/side/sessions") == "$HOME/.grok/sessions" ]] ||
fail "a Grok account shares the CLI and sessions with the primary"
grep -qx -- "--private https://auth.x.ai/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
fail "a second Grok login opens in a private window" "$(cat "$OMARCHY_TEST_BROWSER_LOG")"
omarchy-agent-account-use grok side >/dev/null
source "$ROOT/default/bash/fns/agent-accounts"
[[ $(grok --version) == "grok home=$accounts/grok/side args=--version" ]] || fail "grok starts as the active Grok account" "$(grok --version)"
omarchy-agent-account-use grok main >/dev/null
pass "Grok accounts are added and used like the others"
# ---------------------------------------------------------------------- routing
omarchy-agent-account-use claude work >/dev/null
[[ $(omarchy-agent-account-home claude) == "$work" ]] || fail "the active account's home is what launches use"
grep -q "New Claude sessions now use Work (Max 5x)" "$notifications" || fail "switching says where new sessions go"
pass "use makes an account active and says so"
source "$ROOT/default/bash/fns/agent-accounts"
[[ $(claude --version) == "claude home=$work args=--version" ]] || fail "claude at a prompt starts as the active account"
[[ $(CLAUDE_CONFIG_DIR=/elsewhere claude) == "claude home=/elsewhere args=" ]] || fail "an explicit CLAUDE_CONFIG_DIR wins over the active account"
[[ $(codex) == "codex home=default args=" ]] || fail "codex stays on its primary until switched"
pass "shell launches follow the active account"
[[ $(OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent --inline) == "claude home=$work args=--permission-mode auto" ]] ||
fail "omarchy-agent starts Claude as the active account"
omarchy-agent-account-use codex side >/dev/null
[[ $(OMARCHY_TEST_DEFAULT_AGENT=codex omarchy-agent --inline) == "codex home=$accounts/codex/side args=--approve-for-me" ]] ||
fail "omarchy-agent starts Codex as the active account"
pass "omarchy-agent follows the active account"
omarchy-agent-account-use claude next >/dev/null
[[ -z $(omarchy-agent-account-home claude) ]] || fail "next cycles back to the primary"
pass "next cycles through accounts"
# With Claude as the default agent, the provider can be left out.
OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent-account-use work >/dev/null
[[ $(omarchy-agent-account-home claude) == "$work" ]] || fail "use without a provider picks the default agent's account"
OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent-account-use main >/dev/null
[[ -z $(omarchy-agent-account-home claude) ]] || fail "use without a provider switches back to Main"
OMARCHY_TEST_DEFAULT_AGENT=codex omarchy-agent-account-use main >/dev/null
[[ -z $(omarchy-agent-account-home codex) ]] || fail "the default agent decides which provider a short use means"
if OMARCHY_TEST_DEFAULT_AGENT=pi omarchy-agent-account-use work >/dev/null 2>&1; then
fail "a default agent with no accounts still needs the provider named"
fi
pass "the provider defaults to your default agent"
# ----------------------------------------------------------------------- rename
omarchy-agent-account-use claude work >/dev/null
omarchy-agent-account-rename claude work Day job >/dev/null
[[ $(omarchy-agent-account-list claude --json | jq -c '.[0] | {active, renamed: (.accounts[] | select(.id == "day-job") | .label)}') == '{"active":"day-job","renamed":"Day job"}' ]] ||
fail "renaming changes the label and the id it answers to, and the active account follows"
[[ $(omarchy-agent-account-home claude) == "$work" ]] || fail "renaming leaves the account's home where running sessions expect it"
OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent-account-rename day-job Work >/dev/null
[[ $(omarchy-agent-account-home claude) == "$work" ]] || fail "a renamed account can be renamed back"
omarchy-agent-account-rename claude main Personal >/dev/null
[[ $(omarchy-agent-account-list claude --json | jq -r '.[0].accounts[] | select(.primary) | "\(.id) \(.home)"') == "personal $HOME/.claude" ]] ||
fail "the primary account can be renamed and keeps ~/.claude"
omarchy-agent-account-rename claude personal Main >/dev/null
# `primary` reaches the first login whatever it's called, as the manual says.
omarchy-agent-account-rename claude primary Hey >/dev/null ||
fail "primary names the primary account"
omarchy-agent-account-rename claude primary Main >/dev/null
[[ $(omarchy-agent-account-list claude --json | jq -r '.[0].accounts[] | select(.primary) | .id') == "main" ]] ||
fail "primary still reaches the primary account after a rename"
if omarchy-agent-account-rename claude work "" >/dev/null 2>&1; then
fail "an account can't be renamed to nothing"
fi
pass "rename relabels an account without moving it"
# ------------------------------------------------------------ mode and remove
OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent-account-mode auto 90 >/dev/null
[[ $(jq -c '{switch, threshold}' "$accounts/claude.json") == '{"switch":"auto","threshold":90}' ]] || fail "mode sets switching and threshold"
if omarchy-agent-account-mode claude sometimes >/dev/null 2>&1; then
fail "mode refuses an unknown switch mode"
fi
pass "mode sets how switching happens"
if omarchy-agent-account-remove claude main </dev/null >/dev/null 2>&1; then
fail "the primary account can't be removed"
fi
omarchy-agent-account-use claude work >/dev/null
# A session still running in the account keeps its login until it quits.
CLAUDE_CONFIG_DIR="$work" sleep 30 &
session=$!
if omarchy-agent-account-remove claude work </dev/null >/dev/null 2>"$test_tmp/in-use"; then
fail "an account a running session uses can't be removed"
fi
[[ -d $work ]] && grep -q "Quit it first" "$test_tmp/in-use" || fail "removing an account in use leaves it and says why" "$(cat "$test_tmp/in-use")"
kill "$session"; wait "$session" 2>/dev/null || true
omarchy-agent-account-remove claude work </dev/null >/dev/null
[[ ! -e $work && -d $HOME/.claude/projects && -f $HOME/.claude/settings.json ]] || fail "removing an account deletes its home and nothing it links to"
[[ -z $(omarchy-agent-account-home claude) ]] || fail "removing the active account falls back to the primary"
pass "remove forgets an added account without touching shared files"
# A registry that can't be saved leaves the new login pending, where the add
# command cleans it up, rather than in a home nothing can manage.
STATE="$ROOT/bin/omarchy-agent-account-state" python3 - <<'PY' || fail "a failed registration rolls the new home back to pending"
import importlib.machinery, importlib.util, json, os, sys
loader = importlib.machinery.SourceFileLoader("state", os.environ["STATE"])
spec = importlib.util.spec_from_loader(loader.name, loader)
state = importlib.util.module_from_spec(spec)
loader.exec_module(state)
pending = state.begin("claude")
(pending / ".claude.json").write_text(json.dumps({"oauthAccount": {"accountUuid": "u-stranded", "emailAddress": "stranded@example.com"}}))
def broken_save(provider, registry):
raise OSError("disk full")
state.save = broken_save
try:
state.register("claude", "Stranded", pending)
except OSError:
pass
else:
sys.exit("register should fail when the registry can't be saved")
assert pending.is_dir(), "the login goes back to pending"
assert not (state.accounts_root() / "claude" / "stranded").exists(), "no home is left outside the registry"
# The add command would remove it on exit; this test stands in for it.
import shutil
shutil.rmtree(pending)
PY
pass "a failed registration rolls the new home back to pending"
# ------------------------------------------------------------ panel add flow
[[ $(omarchy-agent-account-add --check) == $'claude additional\ncodex additional\ngrok additional' ]] ||
fail "--check says what adding would mean for each provider" "$(omarchy-agent-account-add --check)"
pass "--check says what adding would mean for each provider"
: >"$notifications"
OMARCHY_TEST_LOGIN_UUID=u-events OMARCHY_TEST_LOGIN_EMAIL=events@example.com \
omarchy-agent-account-add --events claude Events </dev/null >"$test_tmp/events-output"
grep -qx "@@omarchy status Sign in as the account you're adding in the private window that opens." "$test_tmp/events-output" ||
fail "--events reports progress as tagged lines" "$(cat "$test_tmp/events-output")"
grep -qx "@@omarchy done Added Events (events@example.com)." "$test_tmp/events-output" ||
fail "--events reports the result as a tagged line" "$(cat "$test_tmp/events-output")"
grep -q "Added Events (events@example.com)." "$notifications" || fail "--events also notifies, in case the panel closed"
if OMARCHY_TEST_LOGIN_UUID=u-events OMARCHY_TEST_LOGIN_EMAIL=events@example.com \
omarchy-agent-account-add --events claude Again </dev/null >"$test_tmp/events-dup" 2>&1; then
fail "--events fails a duplicate"
fi
grep -q "^@@omarchy error That's Events" "$test_tmp/events-dup" || fail "--events reports a failure as a tagged line" "$(cat "$test_tmp/events-dup")"
pass "--events reports progress and results for the panel"
# ------------------------------------------------------------------- reauth
: >"$OMARCHY_TEST_BROWSER_LOG"
before=$(omarchy-agent-account-list claude --json | jq '.[0].accounts | length')
rm -f "$accounts/claude/events/.credentials.json"
OMARCHY_TEST_LOGIN_UUID=u-events OMARCHY_TEST_LOGIN_EMAIL=events@example.com \
omarchy-agent-account-add --events --reauth events claude </dev/null >"$test_tmp/reauth-output"
grep -qx "@@omarchy done Signed in to Claude again." "$test_tmp/reauth-output" || fail "--reauth reports the sign-in" "$(cat "$test_tmp/reauth-output")"
[[ -s $accounts/claude/events/.credentials.json ]] || fail "--reauth signs in to the account's own home"
[[ $(omarchy-agent-account-list claude --json | jq '.[0].accounts | length') == "$before" ]] || fail "--reauth adds no account"
grep -qx -- "--private https://claude.com/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" || fail "--reauth of an added account uses a private window"
: >"$OMARCHY_TEST_BROWSER_LOG"
OMARCHY_TEST_LOGIN_UUID=u-main OMARCHY_TEST_LOGIN_EMAIL=me@example.com \
omarchy-agent-account-add --reauth main claude </dev/null >/dev/null
grep -qx "default https://claude.com/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" || fail "--reauth of the primary uses the normal browser"
if omarchy-agent-account-add --reauth nobody claude </dev/null >/dev/null 2>&1; then
fail "--reauth of an unknown account fails"
fi
pass "--reauth signs an existing account in again where it lives"
# ------------------------------------------------------------------- cancel
# Cancelling from the panel stops a login that's waiting on the browser, right
# away, and leaves no half-made account behind.
OMARCHY_TEST_LOGIN_HANGS="$test_tmp/login.pid" omarchy-agent-account-add --events claude Slow </dev/null >/dev/null 2>&1 &
adding=$!
sleep 1
started=$(date +%s)
kill -TERM "$adding"
wait "$adding" || true
(( $(date +%s) - started < 3 )) || fail "cancelling stops a waiting login at once"
login_pid=$(cat "$test_tmp/login.pid")
for _ in {1..20}; do
kill -0 "$login_pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$login_pid" 2>/dev/null; then
kill "$login_pid"
fail "cancelling stops the login itself"
fi
[[ -z $(ls -A "$accounts/claude/.pending") ]] || fail "cancelling leaves no half-made account"
pass "cancelling a sign-in stops the login and cleans up"
# ------------------------------------------------------------------ one account
# With one account, the usage record keeps its limits at the top level, and
# the list still shows them.
solo="$test_tmp/solo"
mkdir -p "$solo/omarchy/agents/usage"
echo '{"id":"claude","limits":[{"label":"Session (5-hour)","percent":0.4,"resetsAt":""}]}' >"$solo/omarchy/agents/usage/claude.json"
[[ $(XDG_STATE_HOME="$solo" omarchy-agent-account-list claude --json | jq -c '.[0].accounts[0].limits[0].percent') == "0.4" ]] ||
fail "a single account's limits are listed from the record's top level"
pass "a single account's limits are listed"
# ------------------------------------------------------------- signed out
# A login that's gone is gone, whatever the registry remembered about it.
rm -f "$HOME/.codex/auth.json"
[[ $(omarchy-agent-account-add --check | grep '^codex ') == "codex first" ]] ||
fail "a signed-out primary counts as a first sign-in again" "$(omarchy-agent-account-add --check)"
pass "a signed-out account no longer counts as signed in"
# --------------------------------------------------------------- refresh
# A home signed in to someone else since it was added is saved as who it is
# now, which is what the usage records name it by.
jq '.oauthAccount.emailAddress = "switched@example.com"' "$accounts/claude/events/.claude.json" >"$test_tmp/switched.json"
mv "$test_tmp/switched.json" "$accounts/claude/events/.claude.json"
omarchy-agent-account-state refresh claude
[[ $(jq -r '.accounts[] | select(.id == "events") | .email' "$accounts/claude.json") == "switched@example.com" ]] ||
fail "refresh saves who each home is signed in as now"
pass "refresh saves who each home is signed in as now"