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():