"""Tests for :mod:`omnigent.onboarding.harness_install`.""" from __future__ import annotations import subprocess import pytest from omnigent.onboarding import harness_install as hi from omnigent.onboarding.provider_config import ANTHROPIC_FAMILY, GEMINI_FAMILY, OPENAI_FAMILY @pytest.mark.parametrize( "key,binary,package", [ (ANTHROPIC_FAMILY, "claude", "@anthropic-ai/claude-code"), (OPENAI_FAMILY, "codex", "@openai/codex"), (hi.PI_KEY, "pi", "@earendil-works/pi-coding-agent"), (hi.QWEN_KEY, "qwen", "@qwen-code/qwen-code"), ], ) def test_install_spec_and_command(key: str, binary: str, package: str) -> None: """Each known harness maps to the ucode-matching binary + npm package. A drift in binary/package (e.g. a wrong npm name) would install the wrong thing or check the wrong PATH entry — caught here. """ spec = hi.harness_install_spec(key) assert spec is not None assert spec.binary == binary assert spec.package == package assert hi.harness_install_command(key) == ["npm", "install", "-g", package] def test_kimi_install_spec_is_login_only_no_npm() -> None: """Kimi ships via a curl installer (no npm package) and authenticates through its own ``kimi login`` (OAuth or Moonshot API key), so it carries an ``install_hint`` instead of a ``package`` and intentionally has no ``status_args`` (no exit-code "am I logged in?" probe to read). """ spec = hi.harness_install_spec(hi.KIMI_KEY) assert spec is not None assert spec.binary == "kimi" assert spec.package is None assert spec.install_hint is not None and "code.kimi.com" in spec.install_hint assert spec.login_args == ("login",) assert spec.logout_args == ("logout",) assert spec.status_args is None def test_kimi_required_cli_returns_install_spec() -> None: """The kimi harness is binary-gated: it cannot launch without ``kimi`` on PATH, so the sub-agent dispatch preflight must surface the install spec.""" spec = hi.required_cli_for_harness("kimi") assert spec is not None assert spec.binary == "kimi" def test_kimi_only_upstream_binary_satisfies_readiness( monkeypatch: pytest.MonkeyPatch, ) -> None: """Only ``kimi`` (the upstream MoonshotAI/Kimi-Code binary) counts as installed. The legacy pypi ``kimi-cli`` package is intentionally NOT accepted — its command-line surface is incompatible with what the executor drives, so falsely reading it as configured would crash at the first turn.""" monkeypatch.setattr( hi.shutil, "which", lambda name: "/Users/x/.local/bin/kimi-cli" if name == "kimi-cli" else None, ) assert hi.harness_cli_installed(hi.KIMI_KEY) is False monkeypatch.setattr( hi.shutil, "which", lambda name: "/Users/x/.kimi-code/bin/kimi" if name == "kimi" else None, ) assert hi.harness_cli_installed(hi.KIMI_KEY) is True def test_cursor_install_spec_is_login_only_no_npm() -> None: """Cursor ships via a curl installer (no npm package) and authenticates through its own CLI login, so it carries an ``install_hint`` + status JSON key instead of a ``package``. Drift here (a package sneaking in, or the wrong status key) would make the setup menu offer a bogus ``npm install`` or misread login state. """ spec = hi.harness_install_spec(hi.CURSOR_KEY) assert spec is not None assert spec.binary == "cursor-agent" assert spec.package is None assert spec.install_hint is not None and "cursor.com/install" in spec.install_hint assert spec.login_args == ("login",) assert spec.logout_args == ("logout",) assert spec.status_args == ("status", "--format", "json") assert spec.login_status_key == "isAuthenticated" def test_kiro_install_spec_is_manual_installer_no_npm() -> None: """Kiro ships as a standalone native installer, not an npm package.""" spec = hi.harness_install_spec(hi.KIRO_KEY) assert spec is not None assert spec.display == "Kiro" assert spec.binary == "kiro-cli" assert spec.package is None assert spec.install_hint == "curl -fsSL https://cli.kiro.dev/install | bash" def test_antigravity_install_spec_status_only_no_npm() -> None: """Antigravity (agy) ships via a shell installer (no npm) and has no login subcommand — the user signs in by launching ``agy`` once. It DOES expose a status check (``agy models``), so the spec carries ``status_args`` + ``install_hint`` but no ``package`` / ``login_args`` / ``logout_args``. Drift here (a package sneaking in, or losing ``status_args``) would make the setup menu offer a bogus ``npm install`` or fall back to a file-only login check that can't see server-side revocation. """ spec = hi.harness_install_spec(GEMINI_FAMILY) assert spec is not None assert spec.binary == "agy" assert spec.package is None assert spec.install_hint is not None assert "antigravity.google/cli/install.sh" in spec.install_hint assert spec.status_args == ("models",) assert spec.login_args is None assert spec.logout_args is None assert spec.login_status_key is None assert spec.auth_hint is not None def test_harness_setup_hint_antigravity_surfaces_sign_in() -> None: """A not-yet-signed-in agy can't be fixed by ``agy login`` (no such command), so the launch hint names the installer AND the "run agy to sign in" step — otherwise a user who already has agy installed gets a misleading install-only hint. """ hint = hi.harness_setup_hint("antigravity-native") assert "antigravity.google/cli/install.sh" in hint assert "agy" in hint assert "sign" in hint.lower() def test_install_command_rejects_non_npm_harness() -> None: """A non-npm harness has no npm install command; asking for one is a loud error so the caller shows its ``install_hint`` instead.""" with pytest.raises(ValueError): hi.harness_install_command(hi.CURSOR_KEY) with pytest.raises(ValueError): hi.harness_install_command(hi.KIRO_KEY) def test_install_harness_cli_noop_for_non_npm(monkeypatch: pytest.MonkeyPatch) -> None: """``install_harness_cli`` never shells npm for a non-npm CLI. It returns ``False`` without spawning anything, so the menu falls back to the manual ``install_hint`` rather than running a bogus npm command. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _explode(*a: object, **k: object) -> None: raise AssertionError("npm install spawned for a non-npm harness") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.install_harness_cli(hi.CURSOR_KEY) is False assert hi.install_harness_cli(hi.KIRO_KEY) is False def test_unknown_key_has_no_spec_and_is_not_installed() -> None: """A family with no dedicated CLI (e.g. a gateway-only family) → None / False, never a crash.""" assert hi.harness_install_spec("gateway") is None assert hi.harness_cli_installed("gateway") is False @pytest.mark.parametrize( "harness,binary", [ ("claude-native", "claude"), ("codex-native", "codex"), ("pi", "pi"), # Native Cursor wraps the cursor-agent CLI (distinct from the SDK # ``cursor`` harness, which needs no binary — see the test below). ("cursor-native", "cursor-agent"), ("native-cursor", "cursor-agent"), ("kiro-native", "kiro-cli"), ("native-kiro", "kiro-cli"), ], ) def test_required_cli_for_cli_backed_harness(harness: str, binary: str) -> None: """The CLI-backed harnesses map to the binary their launch needs. Drift here (a wrong/missing mapping) would let sub-agent dispatch skip the preflight for a harness that actually needs a CLI, reintroducing the lazy-boot-failure the guard exists to prevent. """ spec = hi.required_cli_for_harness(harness) assert spec is not None assert spec.binary == binary @pytest.mark.parametrize("harness", ["cursor-native", "native-cursor"]) def test_setup_hint_for_native_cursor_points_at_vendor_installer(harness: str) -> None: """Native Cursor's "not configured" hint names the curl installer + login, never ``omnigent setup`` — which only configures the SDK ``cursor`` harness (``cursor-sdk`` + ``CURSOR_API_KEY``) and never installs ``cursor-agent``. A regression to the generic hint sends a native-Cursor user down a dead end (the exact bug this fixes). """ hint = hi.harness_setup_hint(harness) assert "cursor-agent" in hint assert "cursor.com/install" in hint assert "cursor-agent login" in hint assert "omnigent setup" not in hint @pytest.mark.parametrize("harness", ["kiro-native", "native-kiro"]) def test_setup_hint_for_native_kiro_points_at_vendor_installer(harness: str) -> None: """Native Kiro's missing-binary hint names Kiro's installer, not setup.""" hint = hi.harness_setup_hint(harness) assert "kiro-cli" in hint assert "cli.kiro.dev/install" in hint assert "omnigent setup" not in hint @pytest.mark.parametrize("harness", ["claude-native", "codex", "pi", "claude-sdk", None]) def test_setup_hint_defaults_to_omnigent_setup(harness: str | None) -> None: """Harnesses whose CLI ``omnigent setup`` installs (npm CLIs) — and the SDK / unknown / ``None`` cases — route to the ``omnigent setup`` hint.""" hint = hi.harness_setup_hint(harness) assert "omnigent setup" in hint @pytest.mark.parametrize("harness", ["cursor", "claude-sdk", "openai-agents"]) def test_sdk_harnesses_require_no_cli(harness: str) -> None: """SDK-based harnesses (incl. ``cursor``, which drives the cursor-sdk Python package) require no CLI binary, so the sub-agent dispatch preflight must not flag them — otherwise it would block a launch that needs no CLI (and, for cursor, print ``npm install -g None`` for its package-less spec).""" assert hi.required_cli_for_harness(harness) is None assert hi.missing_harness_cli(harness) is None @pytest.mark.parametrize( "harness", ["claude-sdk", "codex", "openai-agents-sdk", "unknown"], ) def test_required_cli_none_for_sdk_or_unknown_harness(harness: str) -> None: """SDK-based / unknown harnesses need no CLI binary → ``None``. A false positive here would block a perfectly launchable in-process harness (e.g. the claude-sdk orchestrator brain) at dispatch. """ assert hi.required_cli_for_harness(harness) is None def test_missing_harness_cli_present_returns_none(monkeypatch: pytest.MonkeyPatch) -> None: """Binary on PATH → no missing-CLI verdict (dispatch proceeds). A failure here would mean the guard blocks a worker whose CLI is actually installed. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") assert hi.missing_harness_cli("pi") is None def test_missing_harness_cli_absent_returns_spec(monkeypatch: pytest.MonkeyPatch) -> None: """Binary absent from PATH → returns the spec so dispatch can fail loud. This is exactly the pi-not-installed case the guard catches; a failure means the missing CLI would slip through to a lazy boot failure instead. """ monkeypatch.setattr(hi.shutil, "which", lambda name: None) spec = hi.missing_harness_cli("pi") assert spec is not None # The returned spec carries the binary + npm package the dispatch error # surfaces to the orchestrator/human. assert spec.binary == "pi" assert spec.package == "@earendil-works/pi-coding-agent" def test_missing_harness_cli_none_for_sdk_harness(monkeypatch: pytest.MonkeyPatch) -> None: """An SDK harness is never blocked, even when no binary is on PATH. ``shutil.which`` returns None for everything here; the guard must still pass an SDK harness through because it needs no CLI to boot. """ monkeypatch.setattr(hi.shutil, "which", lambda name: None) assert hi.missing_harness_cli("claude-sdk") is None def test_cli_installed_reflects_which(monkeypatch: pytest.MonkeyPatch) -> None: """``harness_cli_installed`` is exactly ``shutil.which(binary) is not None``. Present → True; absent → False — the signal the configure ✗ marker and the run gating both read. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") assert hi.harness_cli_installed(ANTHROPIC_FAMILY) is True monkeypatch.setattr(hi.shutil, "which", lambda name: None) assert hi.harness_cli_installed(ANTHROPIC_FAMILY) is False def test_install_harness_cli_requires_npm(monkeypatch: pytest.MonkeyPatch) -> None: """No npm on PATH → install short-circuits to False without shelling out.""" monkeypatch.setattr(hi.shutil, "which", lambda name: None) def _explode(*a: object, **k: object) -> None: raise AssertionError("subprocess.run reached despite missing npm") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.install_harness_cli(ANTHROPIC_FAMILY) is False def test_install_harness_cli_runs_npm_then_rechecks(monkeypatch: pytest.MonkeyPatch) -> None: """Installs via ``npm install -g `` and reports the post-install PATH state (True once the binary appears).""" calls: list[list[str]] = [] # npm present; the target binary appears only after the install runs. state = {"installed": False} def _which(name: str) -> str | None: if name == "npm": return "/usr/bin/npm" if name == "codex": return "/usr/bin/codex" if state["installed"] else None return None def _run(argv: list[str], *, check: bool = False, timeout: float | None = None): calls.append(argv) state["installed"] = True return subprocess.CompletedProcess(args=argv, returncode=0) monkeypatch.setattr(hi.shutil, "which", _which) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.install_harness_cli(OPENAI_FAMILY) is True assert calls == [["npm", "install", "-g", "@openai/codex"]] def test_harness_login_skips_when_already_logged_in(monkeypatch: pytest.MonkeyPatch) -> None: """An already-logged-in CLI short-circuits to True without spawning login. A failure here means we'd re-run an interactive OAuth flow on a user who is already signed in. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") monkeypatch.setattr( "omnigent.onboarding.harness_install.harness_cli_logged_in", lambda key: True ) def _explode(*a: object, **k: object) -> None: raise AssertionError("login subprocess spawned despite already being logged in") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.harness_login(ANTHROPIC_FAMILY) is True @pytest.mark.parametrize( "key,expected_argv", [ (ANTHROPIC_FAMILY, ["claude", "auth", "login", "--claudeai"]), (OPENAI_FAMILY, ["codex", "login"]), ], ) def test_harness_login_runs_cli_login_then_verifies( monkeypatch: pytest.MonkeyPatch, key: str, expected_argv: list[str] ) -> None: """Not logged in → runs the harness's first-class login argv, then verifies. Asserts the exact argv so a drift away from ``claude auth login --claudeai`` / ``codex login`` (e.g. back to a TUI hack) is caught, and that the result reflects the post-login verdict. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") # Pin stdin to a TTY so this test stays focused on argv and never touches a # real /dev/tty — the non-TTY branch is exercised separately below. monkeypatch.setattr(hi.sys.stdin, "isatty", lambda: True) calls: list[list[str]] = [] state = {"logged_in": False} monkeypatch.setattr( "omnigent.onboarding.harness_install.harness_cli_logged_in", lambda k: state["logged_in"], ) def _run(argv: list[str], **kwargs: object): calls.append(argv) state["logged_in"] = True # the user completed the interactive login return subprocess.CompletedProcess(args=argv, returncode=0) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_login(key) is True assert calls == [expected_argv] def test_harness_login_wires_dev_tty_when_stdin_not_a_tty( monkeypatch: pytest.MonkeyPatch, ) -> None: """No TTY on stdin → open /dev/tty, pass it as the child's std* fds, then close it. When the parent's stdio is piped (e.g. launched via ``uv tool run``) the harness CLI sees ``isatty() == False`` and refuses to open the browser, stranding the login. The fix opens ``/dev/tty`` and hands it to the child as stdin/stdout/stderr so it sees a real terminal. Asserts that wiring happens and that the fd is released even on the success path (``finally``). """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") monkeypatch.setattr(hi.sys.stdin, "isatty", lambda: False) state = {"logged_in": False} monkeypatch.setattr( "omnigent.onboarding.harness_install.harness_cli_logged_in", lambda k: state["logged_in"], ) sentinel_fd = 4242 monkeypatch.setattr(hi.os, "open", lambda path, flags: sentinel_fd) closed: list[int] = [] monkeypatch.setattr(hi.os, "close", lambda fd: closed.append(fd)) seen: dict = {} def _run(argv: list[str], **kwargs: object): seen["kwargs"] = kwargs state["logged_in"] = True return subprocess.CompletedProcess(args=argv, returncode=0) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_login(ANTHROPIC_FAMILY) is True assert seen["kwargs"]["stdin"] == sentinel_fd assert seen["kwargs"]["stdout"] == sentinel_fd assert seen["kwargs"]["stderr"] == sentinel_fd assert closed == [sentinel_fd] # fd released after the login returns def test_harness_login_falls_back_when_dev_tty_unavailable( monkeypatch: pytest.MonkeyPatch, ) -> None: """No controlling terminal → swallow the OSError and inherit parent stdio. Headless / CI runs have no ``/dev/tty``; the login must still proceed with the parent's inherited stdio rather than crash, and must not pass any std* fds to the child. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") monkeypatch.setattr(hi.sys.stdin, "isatty", lambda: False) state = {"logged_in": False} monkeypatch.setattr( "omnigent.onboarding.harness_install.harness_cli_logged_in", lambda k: state["logged_in"], ) def _no_tty(path: str, flags: int) -> int: raise OSError("no controlling terminal") monkeypatch.setattr(hi.os, "open", _no_tty) seen: dict = {} def _run(argv: list[str], **kwargs: object): seen["kwargs"] = kwargs state["logged_in"] = True return subprocess.CompletedProcess(args=argv, returncode=0) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_login(ANTHROPIC_FAMILY) is True assert "stdin" not in seen["kwargs"] assert "stdout" not in seen["kwargs"] assert "stderr" not in seen["kwargs"] def test_harness_login_false_when_login_not_completed(monkeypatch: pytest.MonkeyPatch) -> None: """Login ran but the CLI still reports no login → False. This is what stops the caller from recording a phantom subscription when the user bails out of (or fails) the OAuth flow. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") monkeypatch.setattr( "omnigent.onboarding.harness_install.harness_cli_logged_in", lambda k: False ) monkeypatch.setattr( hi.subprocess, "run", lambda argv, **k: subprocess.CompletedProcess(args=argv, returncode=1), ) assert hi.harness_login(OPENAI_FAMILY) is False def test_harness_login_false_when_binary_missing(monkeypatch: pytest.MonkeyPatch) -> None: """No CLI binary on PATH → False without spawning anything.""" monkeypatch.setattr(hi.shutil, "which", lambda name: None) def _explode(*a: object, **k: object) -> None: raise AssertionError("login spawned despite missing binary") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.harness_login(ANTHROPIC_FAMILY) is False def test_harness_login_false_for_harness_without_login(monkeypatch: pytest.MonkeyPatch) -> None: """A harness with no login command (Pi) → False without spawning anything.""" monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _explode(*a: object, **k: object) -> None: raise AssertionError("login spawned for a harness with no login_args") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.harness_login(hi.PI_KEY) is False @pytest.mark.parametrize( "key,expected_argv", [ (ANTHROPIC_FAMILY, ["claude", "auth", "logout"]), (OPENAI_FAMILY, ["codex", "logout"]), ], ) def test_harness_logout_runs_cli_logout_then_verifies( monkeypatch: pytest.MonkeyPatch, key: str, expected_argv: list[str] ) -> None: """Runs the harness's own logout argv and reports the logged-out verdict.""" monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") calls: list[list[str]] = [] state = {"logged_in": True} monkeypatch.setattr( "omnigent.onboarding.harness_install.harness_cli_logged_in", lambda k: state["logged_in"], ) def _run(argv: list[str], *, check: bool = False, timeout: float | None = None): calls.append(argv) state["logged_in"] = False return subprocess.CompletedProcess(args=argv, returncode=0) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_logout(key) is True assert calls == [expected_argv] @pytest.mark.parametrize( "stdout,returncode,expected", [ # Claude prints JSON; loggedIn is the verdict regardless of exit code. ('{"loggedIn": true, "authMethod": "claude.ai"}', 0, True), ('{"loggedIn": false}', 1, False), # Exit 0 but loggedIn false → the structured verdict still wins. ('{"loggedIn": false}', 0, False), ], ) def test_harness_cli_logged_in_uses_claude_json_verdict( monkeypatch: pytest.MonkeyPatch, stdout: str, returncode: int, expected: bool ) -> None: """Claude's `auth status` JSON `loggedIn` field is the login verdict. This is the macOS fix: Claude stores creds in the Keychain (no `~/.claude/.credentials.json`), so a file check falsely reports "not logged in" right after a successful login. Asking `claude auth status` reads the real state. Failure here means we'd regress to the file-based check. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _run(argv: list[str], **k: object): assert argv == ["claude", "auth", "status"] # the status subcommand return subprocess.CompletedProcess( args=argv, returncode=returncode, stdout=stdout, stderr="" ) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_cli_logged_in(ANTHROPIC_FAMILY) is expected @pytest.mark.parametrize( "stdout,returncode,expected", [ ("Logged in using an API key - sk-***", 0, True), # non-JSON, exit 0 ("Not logged in", 1, False), # non-JSON, exit 1 ], ) def test_harness_cli_logged_in_codex_uses_exit_code( monkeypatch: pytest.MonkeyPatch, stdout: str, returncode: int, expected: bool ) -> None: """Codex's `login status` is non-JSON, so the exit code is the verdict. Codex exits 0 only when logged in; failure means the non-JSON fallback branch misread the status. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _run(argv: list[str], **k: object): assert argv == ["codex", "login", "status"] # the status subcommand return subprocess.CompletedProcess( args=argv, returncode=returncode, stdout=stdout, stderr="" ) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_cli_logged_in(OPENAI_FAMILY) is expected @pytest.mark.parametrize( "stdout,returncode,expected", [ # Cursor prints JSON with ``isAuthenticated``; the field is the verdict # regardless of exit code. ('{"isAuthenticated": true, "status": "authenticated"}', 0, True), ('{"isAuthenticated": false}', 1, False), ('{"isAuthenticated": false}', 0, False), ], ) def test_harness_cli_logged_in_uses_cursor_json_verdict( monkeypatch: pytest.MonkeyPatch, stdout: str, returncode: int, expected: bool ) -> None: """Cursor's ``status --format json`` reports ``isAuthenticated``. Unlike Claude (``loggedIn``) it uses a different key, so the spec's ``login_status_key`` selects it. A regression would misread cursor login state in the setup menu's ✓/✗ marker. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _run(argv: list[str], **k: object): assert argv == ["cursor-agent", "status", "--format", "json"] return subprocess.CompletedProcess( args=argv, returncode=returncode, stdout=stdout, stderr="" ) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_cli_logged_in(hi.CURSOR_KEY) is expected @pytest.mark.parametrize( "stdout,returncode,expected", [ # ``agy models`` lists models (exit 0) only when signed in. ("Gemini 3.5 Flash (Medium)\nGemini 3.1 Pro (High)\n", 0, True), ("Error: Please sign in to view available models.", 1, False), # Exit code is authoritative for agy (no ``login_status_key``): stdout # that happens to be a JSON object with ``loggedIn`` must NOT override # it, so an exit-0 run still reads as signed in. ('{"loggedIn": false}', 0, True), # Empty stdout (e.g. the list went to stderr) → exit code decides. ("", 0, True), ("", 1, False), ], ) def test_harness_cli_logged_in_agy_uses_exit_code( monkeypatch: pytest.MonkeyPatch, stdout: str, returncode: int, expected: bool ) -> None: """Antigravity's ``agy models`` is non-JSON, so the exit code is the verdict. ``agy`` has no ``login status`` subcommand; ``agy models`` exits 0 only when signed in (else exits non-zero with "Please sign in"). A regression would misread agy login state in the setup menu's ✓/✗ marker. """ monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _run(argv: list[str], **k: object): assert argv == ["agy", "models"] # the status subcommand return subprocess.CompletedProcess( args=argv, returncode=returncode, stdout=stdout, stderr="" ) monkeypatch.setattr(hi.subprocess, "run", _run) assert hi.harness_cli_logged_in(GEMINI_FAMILY) is expected def test_harness_cli_logged_in_false_when_binary_missing(monkeypatch: pytest.MonkeyPatch) -> None: """No CLI binary on PATH → False without spawning a status check.""" monkeypatch.setattr(hi.shutil, "which", lambda name: None) def _explode(*a: object, **k: object) -> None: raise AssertionError("status spawned despite missing binary") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.harness_cli_logged_in(ANTHROPIC_FAMILY) is False def test_harness_cli_logged_in_false_for_harness_without_status( monkeypatch: pytest.MonkeyPatch, ) -> None: """A harness with no status command (Pi) → False without spawning anything.""" monkeypatch.setattr(hi.shutil, "which", lambda name: f"/usr/bin/{name}") def _explode(*a: object, **k: object) -> None: raise AssertionError("status spawned for a harness with no status_args") monkeypatch.setattr(hi.subprocess, "run", _explode) assert hi.harness_cli_logged_in(hi.PI_KEY) is False