From 09054eaebeb06b18222ad8c91863607f6a3a81b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 23:22:37 +0000 Subject: [PATCH] Route root-callback env errors through the standard error path The root @app.callback() was the one place that emitted a CLIError by hand (typer.echo(err.message)), so an unknown --env / AAI_ENV / stored-profile env was the only user-facing error that ignored --json, dropped the Error:/Suggestion: formatting, and gave no recovery hint. - environments.get() now attaches a suggestion covering all three sources of the bad name (flag, AAI_ENV, profile). - main() emits the error via output.emit_error (auto-detecting JSON when piped/ agentic) and styles the env-override warning via output.warn, matching every other command path. https://claude.ai/code/session_01QdhBPbDcDgoTnefyTzZK2W --- aai_cli/environments.py | 5 +++++ aai_cli/main.py | 9 ++++++--- tests/test_environments.py | 3 +++ tests/test_login.py | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/aai_cli/environments.py b/aai_cli/environments.py index 3739edc9..ded6260d 100644 --- a/aai_cli/environments.py +++ b/aai_cli/environments.py @@ -66,10 +66,15 @@ def get(name: str) -> Environment: """The named environment, or a clean CLIError if it's unknown.""" env = ENVIRONMENTS.get(name) if env is None: + # The bad name can arrive via --env, AAI_ENV, or a profile's stored env, so + # the suggestion points at all three rather than assuming the flag. raise CLIError( f"Unknown environment {name!r}. Known: {', '.join(ENVIRONMENTS)}.", error_type="invalid_environment", exit_code=2, + suggestion=( + f"Pass --env with one of: {', '.join(ENVIRONMENTS)}, or unset AAI_ENV if it's set." + ), ) return env diff --git a/aai_cli/main.py b/aai_cli/main.py index cc367828..8e0f5838 100644 --- a/aai_cli/main.py +++ b/aai_cli/main.py @@ -11,7 +11,7 @@ # context type, not the upstream click.Context. Imported for typing only. from typer._click.core import Context as ClickContext -from aai_cli import __version__, environments, help_panels, stdio +from aai_cli import __version__, environments, help_panels, output, stdio from aai_cli.commands import ( account, agent, @@ -108,14 +108,17 @@ def main( env = "sandbox000" state = AppState(profile=profile, env=env) ctx.obj = state + # The command's own --json flag isn't parsed yet, so fall back to auto-detection + # (JSON when piped/agentic) — the same default run_command uses for its errors. + json_mode = output.resolve_json(explicit=False) try: environments.set_active(resolve_environment(state)) except CLIError as err: - typer.echo(err.message, err=True) + output.emit_error(err, json_mode=json_mode) raise typer.Exit(code=err.exit_code) from None warning = env_override_warning(state) if warning: - typer.echo(warning, err=True) + output.error_console.print(output.warn(warning)) # Help-panel grouping: named sub-typers carry their panel on `add_typer`; merged diff --git a/tests/test_environments.py b/tests/test_environments.py index ee7bfe6f..d4a36026 100644 --- a/tests/test_environments.py +++ b/tests/test_environments.py @@ -21,6 +21,9 @@ def test_get_unknown_raises_cli_error(): with pytest.raises(CLIError) as exc: environments.get("nope") assert exc.value.exit_code == 2 + # Carries a recovery hint covering all three sources of the name (flag/env/profile). + assert exc.value.suggestion is not None + assert "unset AAI_ENV" in exc.value.suggestion def test_resolve_precedence(monkeypatch): diff --git a/tests/test_login.py b/tests/test_login.py index 67aece52..9f80d227 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -202,8 +202,27 @@ def test_root_callback_sandbox_overrides_profile_env(): def test_unknown_env_exits_2(): + import json + result = runner.invoke(app, ["--env", "bogus", "whoami"]) assert result.exit_code == 2 + # Routed through the standard error path: JSON shape (piped/agentic) + a suggestion, + # not a bare echo of the message. + payload = json.loads(result.output.strip().splitlines()[-1]) + assert payload["error"]["type"] == "invalid_environment" + assert "unset AAI_ENV" in payload["error"]["suggestion"] + + +def test_unknown_env_human_output_when_interactive(): + # For an interactive human (not piped/agentic) the callback emits the human + # "Error:" + "Suggestion:" pair, not JSON — pinning that resolve_json(explicit=False) + # actually consults the auto-detection rather than always forcing JSON. + with patch("aai_cli.output._is_agentic", return_value=False): + result = runner.invoke(app, ["--env", "bogus", "whoami"]) + assert result.exit_code == 2 + assert "Error:" in result.output + assert "Suggestion:" in result.output + assert '"error"' not in result.output # not the JSON shape def test_env_override_prints_warning_to_stderr():