Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions aai_cli/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions aai_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions tests/test_environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading