Skip to content

Commit 1c5bb1f

Browse files
tests: split run_agent_cascade wiring tests out of test_live_tui.py
Keeps both files under the 500-line file-length gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7f407ea commit 1c5bb1f

2 files changed

Lines changed: 149 additions & 140 deletions

File tree

tests/test_live_tui.py

Lines changed: 0 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,18 @@
1010

1111
import asyncio
1212
import threading
13-
import types
1413

15-
import pytest
16-
import typer
1714
from textual.widgets import Static
1815

19-
from aai_cli.agent_cascade import engine
2016
from aai_cli.agent_cascade.tui import LiveAgentApp, _TuiRenderer
21-
from aai_cli.app.context import AppState
2217
from aai_cli.code_agent.messages import (
2318
AssistantMessage,
2419
ErrorMessage,
2520
Note,
2621
ToolAffordance,
2722
UserMessage,
2823
)
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
3224
from aai_cli.core.errors import CLIError
33-
from tests.test_agent_cascade_command import _opts
3425

3526

3627
def _run(coro) -> None:
@@ -378,134 +369,3 @@ def test_tui_renderer_drops_calls_after_the_app_stops() -> None:
378369
renderer = _TuiRenderer(app)
379370
renderer.user_final("ignored") # returns without raising
380371
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

tests/test_live_tui_launch.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Tests for run_agent_cascade's TUI selection + wiring.
2+
3+
Split out of test_live_tui.py to keep both files under the 500-line file-length gate.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import types
9+
10+
import pytest
11+
import typer
12+
13+
from aai_cli.agent_cascade import engine
14+
from aai_cli.app.context import AppState
15+
from aai_cli.commands.agent_cascade import _exec
16+
from aai_cli.commands.agent_cascade._exec import run_agent_cascade
17+
from aai_cli.core import config, stdio
18+
from aai_cli.core.errors import CLIError
19+
from tests.test_agent_cascade_command import _opts
20+
21+
# --- run_agent_cascade -> TUI selection + wiring -----------------------------
22+
23+
24+
def test_should_use_tui_only_for_interactive_human_mic_sessions(monkeypatch) -> None:
25+
# The TUI is the default for a live mic session in human mode on a TTY. Each of the four
26+
# disqualifiers (file input, --json, -o text, no TTY) falls back to the line renderer.
27+
monkeypatch.setattr(stdio, "stdout_is_tty", lambda: True)
28+
monkeypatch.setattr(stdio, "stdin_is_tty", lambda: True)
29+
assert _exec._should_use_tui(from_file=False, json_mode=False, text_mode=False) is True
30+
assert _exec._should_use_tui(from_file=True, json_mode=False, text_mode=False) is False
31+
assert _exec._should_use_tui(from_file=False, json_mode=True, text_mode=False) is False
32+
assert _exec._should_use_tui(from_file=False, json_mode=False, text_mode=True) is False
33+
monkeypatch.setattr(stdio, "stdout_is_tty", lambda: False)
34+
assert _exec._should_use_tui(from_file=False, json_mode=False, text_mode=False) is False
35+
36+
37+
def test_web_search_note_tracks_the_firecrawl_key(monkeypatch) -> None:
38+
monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False)
39+
assert "FIRECRAWL_API_KEY" in (_exec._web_search_note() or "")
40+
monkeypatch.setenv("FIRECRAWL_API_KEY", "fc-x")
41+
assert _exec._web_search_note() is None
42+
43+
44+
def _wire_tui(monkeypatch):
45+
"""Stub auth/audio/deps so run_agent_cascade reaches the TUI launch on an interactive mic run."""
46+
monkeypatch.setattr(_exec.tts_session, "require_available", lambda _c: None)
47+
monkeypatch.setattr(config, "resolve_api_key", lambda **_: "k")
48+
monkeypatch.setattr(stdio, "stdout_is_tty", lambda: True)
49+
monkeypatch.setattr(stdio, "stdin_is_tty", lambda: True)
50+
fake_duplex = types.SimpleNamespace(mic=object(), player=object(), close=lambda: None)
51+
monkeypatch.setattr(_exec, "DuplexAudio", lambda **kwargs: fake_duplex)
52+
monkeypatch.setattr(engine.CascadeDeps, "real", lambda *a, **k: "deps")
53+
return fake_duplex
54+
55+
56+
def test_interactive_human_run_launches_the_tui(monkeypatch) -> None:
57+
# A mic session in human mode on a TTY runs the Textual app, not the line renderer.
58+
fake_duplex = _wire_tui(monkeypatch)
59+
captured: dict[str, object] = {}
60+
61+
class FakeApp:
62+
error = None # no fatal leg failure -> the launcher re-raises nothing
63+
64+
def __init__(self, *, run_conversation, on_stop, web_note):
65+
captured["run_conversation"] = run_conversation
66+
captured["on_stop"] = on_stop
67+
68+
def run(self, **kwargs):
69+
captured["ran"] = kwargs
70+
71+
monkeypatch.setattr("aai_cli.agent_cascade.tui.LiveAgentApp", FakeApp)
72+
# AgentRenderer must NOT be built on the TUI path — fail loudly if the line path is taken.
73+
monkeypatch.setattr(
74+
_exec, "AgentRenderer", lambda **kw: pytest.fail("line renderer used in TUI mode")
75+
)
76+
run_agent_cascade(_opts(), AppState(), json_mode=False)
77+
assert callable(captured["run_conversation"]) # the TUI was launched with a cascade closure
78+
assert captured["on_stop"] is fake_duplex.close # quit closes the audio
79+
assert captured["ran"] == {"mouse": False} # mouse off so transcript text stays selectable
80+
81+
82+
def test_tui_setup_keyboard_interrupt_exits_clean(monkeypatch) -> None:
83+
# Ctrl-C during TUI setup (mic open / graph build / --mcp-config load) lands before
84+
# Textual captures the keyboard; it must exit 130, not surface a raw traceback.
85+
_wire_tui(monkeypatch)
86+
87+
def boom(*_a, **_k):
88+
raise KeyboardInterrupt
89+
90+
monkeypatch.setattr(_exec, "_run_live_tui", boom)
91+
with pytest.raises(typer.Exit) as exc:
92+
run_agent_cascade(_opts(), AppState(), json_mode=False)
93+
assert exc.value.exit_code == 130
94+
95+
96+
def test_tui_run_conversation_drives_the_cascade(monkeypatch) -> None:
97+
# The closure handed to the app runs the cascade with the duplex player and the wired
98+
# deps, and the cascade's on_session wires the session's reply-interrupt onto the app.
99+
fake_duplex = _wire_tui(monkeypatch)
100+
captured: dict[str, object] = {}
101+
102+
def fake_run_cascade(**kw):
103+
captured.update(kw)
104+
# run_cascade hands the freshly built session to on_session before the conversation.
105+
kw["on_session"](types.SimpleNamespace(interrupt_reply="session-interrupt"))
106+
107+
monkeypatch.setattr(engine, "run_cascade", fake_run_cascade)
108+
109+
class FakeApp:
110+
error = None # the conversation completes cleanly here
111+
112+
def __init__(self, *, run_conversation, on_stop, web_note):
113+
self._rc = run_conversation
114+
115+
def run(self, **kwargs):
116+
self._rc("renderer-sentinel") # the app would call this on its worker thread
117+
118+
def set_interrupt(self, interrupt):
119+
captured["interrupt"] = interrupt
120+
121+
monkeypatch.setattr("aai_cli.agent_cascade.tui.LiveAgentApp", FakeApp)
122+
run_agent_cascade(_opts(), AppState(), json_mode=False)
123+
assert captured["player"] is fake_duplex.player
124+
assert captured["deps"] == "deps"
125+
assert captured["renderer"] == "renderer-sentinel"
126+
# The session's interrupt_reply was wired onto the app (so Escape/Ctrl-C can use it).
127+
assert captured["interrupt"] == "session-interrupt"
128+
129+
130+
def test_tui_reraises_a_fatal_leg_error_for_the_exit_code(monkeypatch) -> None:
131+
# A fatal leg failure is caught on the TUI worker thread and parked on app.error; the
132+
# launcher must re-raise it after the app tears down so the command exits with the
133+
# error's code (api_error -> exit 1) instead of a silent success.
134+
_wire_tui(monkeypatch)
135+
boom = CLIError("streaming STT closed", error_type="api_error", exit_code=1)
136+
137+
class FakeApp:
138+
error = boom # the worker thread recorded a fatal cascade error
139+
140+
def __init__(self, *, run_conversation, on_stop, web_note):
141+
pass
142+
143+
def run(self, **kwargs):
144+
pass
145+
146+
monkeypatch.setattr("aai_cli.agent_cascade.tui.LiveAgentApp", FakeApp)
147+
with pytest.raises(CLIError) as exc:
148+
run_agent_cascade(_opts(), AppState(), json_mode=False)
149+
assert exc.value is boom

0 commit comments

Comments
 (0)