|
10 | 10 |
|
11 | 11 | import asyncio |
12 | 12 | import threading |
13 | | -import types |
14 | 13 |
|
15 | | -import pytest |
16 | | -import typer |
17 | 14 | from textual.widgets import Static |
18 | 15 |
|
19 | | -from aai_cli.agent_cascade import engine |
20 | 16 | from aai_cli.agent_cascade.tui import LiveAgentApp, _TuiRenderer |
21 | | -from aai_cli.app.context import AppState |
22 | 17 | from aai_cli.code_agent.messages import ( |
23 | 18 | AssistantMessage, |
24 | 19 | ErrorMessage, |
25 | 20 | Note, |
26 | 21 | ToolAffordance, |
27 | 22 | UserMessage, |
28 | 23 | ) |
29 | | -from aai_cli.commands.agent_cascade import _exec |
30 | | -from aai_cli.commands.agent_cascade._exec import run_agent_cascade |
31 | | -from aai_cli.core import config, stdio |
32 | 24 | from aai_cli.core.errors import CLIError |
33 | | -from tests.test_agent_cascade_command import _opts |
34 | 25 |
|
35 | 26 |
|
36 | 27 | def _run(coro) -> None: |
@@ -378,134 +369,3 @@ def test_tui_renderer_drops_calls_after_the_app_stops() -> None: |
378 | 369 | renderer = _TuiRenderer(app) |
379 | 370 | renderer.user_final("ignored") # returns without raising |
380 | 371 | renderer.reply_done(interrupted=False) |
381 | | - |
382 | | - |
383 | | -# --- run_agent_cascade -> TUI selection + wiring ----------------------------- |
384 | | - |
385 | | - |
386 | | -def test_should_use_tui_only_for_interactive_human_mic_sessions(monkeypatch) -> None: |
387 | | - # The TUI is the default for a live mic session in human mode on a TTY. Each of the four |
388 | | - # disqualifiers (file input, --json, -o text, no TTY) falls back to the line renderer. |
389 | | - monkeypatch.setattr(stdio, "stdout_is_tty", lambda: True) |
390 | | - monkeypatch.setattr(stdio, "stdin_is_tty", lambda: True) |
391 | | - assert _exec._should_use_tui(from_file=False, json_mode=False, text_mode=False) is True |
392 | | - assert _exec._should_use_tui(from_file=True, json_mode=False, text_mode=False) is False |
393 | | - assert _exec._should_use_tui(from_file=False, json_mode=True, text_mode=False) is False |
394 | | - assert _exec._should_use_tui(from_file=False, json_mode=False, text_mode=True) is False |
395 | | - monkeypatch.setattr(stdio, "stdout_is_tty", lambda: False) |
396 | | - assert _exec._should_use_tui(from_file=False, json_mode=False, text_mode=False) is False |
397 | | - |
398 | | - |
399 | | -def test_web_search_note_tracks_the_firecrawl_key(monkeypatch) -> None: |
400 | | - monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False) |
401 | | - assert "FIRECRAWL_API_KEY" in (_exec._web_search_note() or "") |
402 | | - monkeypatch.setenv("FIRECRAWL_API_KEY", "fc-x") |
403 | | - assert _exec._web_search_note() is None |
404 | | - |
405 | | - |
406 | | -def _wire_tui(monkeypatch): |
407 | | - """Stub auth/audio/deps so run_agent_cascade reaches the TUI launch on an interactive mic run.""" |
408 | | - monkeypatch.setattr(_exec.tts_session, "require_available", lambda _c: None) |
409 | | - monkeypatch.setattr(config, "resolve_api_key", lambda **_: "k") |
410 | | - monkeypatch.setattr(stdio, "stdout_is_tty", lambda: True) |
411 | | - monkeypatch.setattr(stdio, "stdin_is_tty", lambda: True) |
412 | | - fake_duplex = types.SimpleNamespace(mic=object(), player=object(), close=lambda: None) |
413 | | - monkeypatch.setattr(_exec, "DuplexAudio", lambda **kwargs: fake_duplex) |
414 | | - monkeypatch.setattr(engine.CascadeDeps, "real", lambda *a, **k: "deps") |
415 | | - return fake_duplex |
416 | | - |
417 | | - |
418 | | -def test_interactive_human_run_launches_the_tui(monkeypatch) -> None: |
419 | | - # A mic session in human mode on a TTY runs the Textual app, not the line renderer. |
420 | | - fake_duplex = _wire_tui(monkeypatch) |
421 | | - captured: dict[str, object] = {} |
422 | | - |
423 | | - class FakeApp: |
424 | | - error = None # no fatal leg failure -> the launcher re-raises nothing |
425 | | - |
426 | | - def __init__(self, *, run_conversation, on_stop, web_note): |
427 | | - captured["run_conversation"] = run_conversation |
428 | | - captured["on_stop"] = on_stop |
429 | | - |
430 | | - def run(self, **kwargs): |
431 | | - captured["ran"] = kwargs |
432 | | - |
433 | | - monkeypatch.setattr("aai_cli.agent_cascade.tui.LiveAgentApp", FakeApp) |
434 | | - # AgentRenderer must NOT be built on the TUI path — fail loudly if the line path is taken. |
435 | | - monkeypatch.setattr( |
436 | | - _exec, "AgentRenderer", lambda **kw: pytest.fail("line renderer used in TUI mode") |
437 | | - ) |
438 | | - run_agent_cascade(_opts(), AppState(), json_mode=False) |
439 | | - assert callable(captured["run_conversation"]) # the TUI was launched with a cascade closure |
440 | | - assert captured["on_stop"] is fake_duplex.close # quit closes the audio |
441 | | - assert captured["ran"] == {"mouse": False} # mouse off so transcript text stays selectable |
442 | | - |
443 | | - |
444 | | -def test_tui_setup_keyboard_interrupt_exits_clean(monkeypatch) -> None: |
445 | | - # Ctrl-C during TUI setup (mic open / graph build / --mcp-config load) lands before |
446 | | - # Textual captures the keyboard; it must exit 130, not surface a raw traceback. |
447 | | - _wire_tui(monkeypatch) |
448 | | - |
449 | | - def boom(*_a, **_k): |
450 | | - raise KeyboardInterrupt |
451 | | - |
452 | | - monkeypatch.setattr(_exec, "_run_live_tui", boom) |
453 | | - with pytest.raises(typer.Exit) as exc: |
454 | | - run_agent_cascade(_opts(), AppState(), json_mode=False) |
455 | | - assert exc.value.exit_code == 130 |
456 | | - |
457 | | - |
458 | | -def test_tui_run_conversation_drives_the_cascade(monkeypatch) -> None: |
459 | | - # The closure handed to the app runs the cascade with the duplex player and the wired |
460 | | - # deps, and the cascade's on_session wires the session's reply-interrupt onto the app. |
461 | | - fake_duplex = _wire_tui(monkeypatch) |
462 | | - captured: dict[str, object] = {} |
463 | | - |
464 | | - def fake_run_cascade(**kw): |
465 | | - captured.update(kw) |
466 | | - # run_cascade hands the freshly built session to on_session before the conversation. |
467 | | - kw["on_session"](types.SimpleNamespace(interrupt_reply="session-interrupt")) |
468 | | - |
469 | | - monkeypatch.setattr(engine, "run_cascade", fake_run_cascade) |
470 | | - |
471 | | - class FakeApp: |
472 | | - error = None # the conversation completes cleanly here |
473 | | - |
474 | | - def __init__(self, *, run_conversation, on_stop, web_note): |
475 | | - self._rc = run_conversation |
476 | | - |
477 | | - def run(self, **kwargs): |
478 | | - self._rc("renderer-sentinel") # the app would call this on its worker thread |
479 | | - |
480 | | - def set_interrupt(self, interrupt): |
481 | | - captured["interrupt"] = interrupt |
482 | | - |
483 | | - monkeypatch.setattr("aai_cli.agent_cascade.tui.LiveAgentApp", FakeApp) |
484 | | - run_agent_cascade(_opts(), AppState(), json_mode=False) |
485 | | - assert captured["player"] is fake_duplex.player |
486 | | - assert captured["deps"] == "deps" |
487 | | - assert captured["renderer"] == "renderer-sentinel" |
488 | | - # The session's interrupt_reply was wired onto the app (so Escape/Ctrl-C can use it). |
489 | | - assert captured["interrupt"] == "session-interrupt" |
490 | | - |
491 | | - |
492 | | -def test_tui_reraises_a_fatal_leg_error_for_the_exit_code(monkeypatch) -> None: |
493 | | - # A fatal leg failure is caught on the TUI worker thread and parked on app.error; the |
494 | | - # launcher must re-raise it after the app tears down so the command exits with the |
495 | | - # error's code (api_error -> exit 1) instead of a silent success. |
496 | | - _wire_tui(monkeypatch) |
497 | | - boom = CLIError("streaming STT closed", error_type="api_error", exit_code=1) |
498 | | - |
499 | | - class FakeApp: |
500 | | - error = boom # the worker thread recorded a fatal cascade error |
501 | | - |
502 | | - def __init__(self, *, run_conversation, on_stop, web_note): |
503 | | - pass |
504 | | - |
505 | | - def run(self, **kwargs): |
506 | | - pass |
507 | | - |
508 | | - monkeypatch.setattr("aai_cli.agent_cascade.tui.LiveAgentApp", FakeApp) |
509 | | - with pytest.raises(CLIError) as exc: |
510 | | - run_agent_cascade(_opts(), AppState(), json_mode=False) |
511 | | - assert exc.value is boom |
0 commit comments