refactor(cli): move the live view to cli/display.py and the prompts to cli/prompts.py

- display.py holds the message buffer, layout, status tables and report panels, the analyst wall-time tracker (CLI-only, from graph/analyst_execution) and the one Console
- utils.py is renamed prompts.py, which is what it holds; its analyst list is ANALYST_CHOICES, apart from display's ANALYST_ORDER
- get_initial_analyst_node, a one-line helper with one caller, is inlined
- the wall-time tracker tests sit with the other display tests, and tests import cli.prompts as prompts
This commit is contained in:
Yijia-Xiao
2026-09-24 05:00:36 +00:00
parent 969861e8be
commit 56bd98f690
19 changed files with 827 additions and 803 deletions
-57
View File
@@ -1,10 +1,7 @@
import unittest
from tradingagents.graph.analyst_execution import (
AnalystWallTimeTracker,
build_analyst_execution_plan,
get_initial_analyst_node,
sync_analyst_tracker_from_chunk,
)
@@ -21,14 +18,6 @@ class AnalystExecutionPlanTests(unittest.TestCase):
with self.assertRaises(ValueError):
build_analyst_execution_plan(["market", "macro"])
def test_get_initial_analyst_node_uses_plan_metadata(self):
plan = build_analyst_execution_plan(["fundamentals", "news"])
self.assertEqual(
get_initial_analyst_node(plan),
"Fundamentals Analyst",
)
def test_social_key_displays_as_sentiment_analyst(self):
# The wire key stays "social" for saved-config back-compat, but the
# user-visible agent_node label must match the v0.2.5 rename so the
@@ -39,49 +28,3 @@ class AnalystExecutionPlanTests(unittest.TestCase):
self.assertEqual(spec.key, "social")
self.assertEqual(spec.agent_node, "Sentiment Analyst")
self.assertEqual(spec.report_key, "sentiment_report")
class AnalystWallTimeTrackerTests(unittest.TestCase):
def test_records_wall_time_when_analyst_completes(self):
plan = build_analyst_execution_plan(["market", "news"])
tracker = AnalystWallTimeTracker(plan)
tracker.mark_started("market", started_at=10.0)
tracker.mark_completed("market", completed_at=13.5)
self.assertEqual(tracker.format_summary(), "Analyst wall time: Market 3.50s")
def test_formats_summary_in_plan_order(self):
plan = build_analyst_execution_plan(["news", "market"])
tracker = AnalystWallTimeTracker(plan)
tracker.mark_started("market", started_at=20.0)
tracker.mark_completed("market", completed_at=22.25)
tracker.mark_started("news", started_at=10.0)
tracker.mark_completed("news", completed_at=14.0)
self.assertEqual(
tracker.format_summary(),
"Analyst wall time: News 4.00s | Market 2.25s",
)
def test_syncs_wall_time_from_sequential_chunks(self):
plan = build_analyst_execution_plan(["market", "news"])
tracker = AnalystWallTimeTracker(plan)
sync_analyst_tracker_from_chunk(tracker, {}, now=10.0)
self.assertEqual(tracker.format_summary(), "Analyst wall time: pending")
sync_analyst_tracker_from_chunk(
tracker,
{"market_report": "done"},
now=13.0,
)
self.assertEqual(tracker.format_summary(), "Analyst wall time: Market 3.00s")
sync_analyst_tracker_from_chunk(
tracker,
{"market_report": "done", "news_report": "done"},
now=18.0,
)
self.assertEqual(tracker.format_summary(), "Analyst wall time: Market 3.00s | News 5.00s")
+32 -32
View File
@@ -15,7 +15,7 @@ from tradingagents.llm_clients.api_key_env import PROVIDER_API_KEY_ENV, get_api_
def test_every_select_llm_provider_choice_has_an_entry():
"""select_llm_provider() must not present a provider the mapping doesn't know about."""
# Mirrors the dropdown order in cli/utils.select_llm_provider so the two
# Mirrors the dropdown order in cli/prompts.select_llm_provider so the two
# stay in lockstep. Region-specific keys (qwen-cn / minimax-cn / glm-cn)
# are reached via the secondary region prompt, so they must also be present.
expected = {
@@ -67,44 +67,44 @@ def test_case_insensitive_lookup():
@pytest.fixture
def cli_utils(monkeypatch):
"""Import cli.utils with a fresh environment so module-level state is consistent."""
def prompts(monkeypatch):
"""Import cli.prompts with a fresh environment so module-level state is consistent."""
import importlib
import cli.utils as cli_utils_module
return importlib.reload(cli_utils_module)
import cli.prompts as prompts_module
return importlib.reload(prompts_module)
def test_ensure_api_key_returns_existing(monkeypatch, cli_utils):
def test_ensure_api_key_returns_existing(monkeypatch, prompts):
monkeypatch.setenv("OPENAI_API_KEY", "sk-already-set")
result = cli_utils.ensure_api_key("openai")
result = prompts.ensure_api_key("openai")
assert result == "sk-already-set"
def test_ensure_api_key_no_op_for_ollama(monkeypatch, cli_utils):
def test_ensure_api_key_no_op_for_ollama(monkeypatch, prompts):
# Even with no env var set, ollama should not prompt and should return None.
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
with patch.object(cli_utils, "questionary") as mock_q:
result = cli_utils.ensure_api_key("ollama")
with patch.object(prompts, "questionary") as mock_q:
result = prompts.ensure_api_key("ollama")
assert result is None
mock_q.password.assert_not_called()
def test_ensure_api_key_unknown_provider_no_prompt(monkeypatch, cli_utils):
with patch.object(cli_utils, "questionary") as mock_q:
result = cli_utils.ensure_api_key("totally-fake-provider")
def test_ensure_api_key_unknown_provider_no_prompt(monkeypatch, prompts):
with patch.object(prompts, "questionary") as mock_q:
result = prompts.ensure_api_key("totally-fake-provider")
assert result is None
mock_q.password.assert_not_called()
def test_ensure_api_key_prompts_and_writes_to_env(monkeypatch, tmp_path, cli_utils):
def test_ensure_api_key_prompts_and_writes_to_env(monkeypatch, tmp_path, prompts):
"""When key is missing, user-pasted value must be written to .env AND os.environ."""
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
monkeypatch.chdir(tmp_path)
fake_prompt = type("P", (), {"ask": staticmethod(lambda: "sk-deepseek-test")})()
with patch.object(cli_utils.questionary, "password", return_value=fake_prompt):
result = cli_utils.ensure_api_key("deepseek")
with patch.object(prompts.questionary, "password", return_value=fake_prompt):
result = prompts.ensure_api_key("deepseek")
assert result == "sk-deepseek-test"
assert os.environ["DEEPSEEK_API_KEY"] == "sk-deepseek-test"
@@ -114,14 +114,14 @@ def test_ensure_api_key_prompts_and_writes_to_env(monkeypatch, tmp_path, cli_uti
assert "sk-deepseek-test" in env_file.read_text()
def test_ensure_api_key_user_cancels_returns_none(monkeypatch, tmp_path, cli_utils):
def test_ensure_api_key_user_cancels_returns_none(monkeypatch, tmp_path, prompts):
"""Empty prompt response (user cancelled) must not write to .env."""
monkeypatch.delenv("XAI_API_KEY", raising=False)
monkeypatch.chdir(tmp_path)
fake_prompt = type("P", (), {"ask": staticmethod(lambda: None)})()
with patch.object(cli_utils.questionary, "password", return_value=fake_prompt):
result = cli_utils.ensure_api_key("xai")
with patch.object(prompts.questionary, "password", return_value=fake_prompt):
result = prompts.ensure_api_key("xai")
assert result is None
assert "XAI_API_KEY" not in os.environ
@@ -132,7 +132,7 @@ def test_ensure_api_key_user_cancels_returns_none(monkeypatch, tmp_path, cli_uti
assert "XAI_API_KEY" not in env_file.read_text()
def test_ensure_api_key_updates_existing_env_file(monkeypatch, tmp_path, cli_utils):
def test_ensure_api_key_updates_existing_env_file(monkeypatch, tmp_path, prompts):
"""An existing .env with other keys must be preserved on writeback."""
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.chdir(tmp_path)
@@ -140,8 +140,8 @@ def test_ensure_api_key_updates_existing_env_file(monkeypatch, tmp_path, cli_uti
env_file.write_text("OPENAI_API_KEY=sk-existing\nOTHER=value\n")
fake_prompt = type("P", (), {"ask": staticmethod(lambda: "sk-openrouter-new")})()
with patch.object(cli_utils.questionary, "password", return_value=fake_prompt):
cli_utils.ensure_api_key("openrouter")
with patch.object(prompts.questionary, "password", return_value=fake_prompt):
prompts.ensure_api_key("openrouter")
content = env_file.read_text()
assert "OPENAI_API_KEY" in content and "sk-existing" in content
@@ -149,22 +149,22 @@ def test_ensure_api_key_updates_existing_env_file(monkeypatch, tmp_path, cli_uti
assert "OPENROUTER_API_KEY" in content and "sk-openrouter-new" in content
def _prompt_key(cli_utils, monkeypatch, tmp_path, key="sk-typed-in"):
def _prompt_key(prompts, monkeypatch, tmp_path, key="sk-typed-in"):
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setattr(cli_utils, "find_dotenv", lambda **k: "")
with patch.object(cli_utils, "questionary") as mock_q:
monkeypatch.setattr(prompts, "find_dotenv", lambda **k: "")
with patch.object(prompts, "questionary") as mock_q:
mock_q.password.return_value.ask.return_value = key
cli_utils.ensure_api_key("openai")
prompts.ensure_api_key("openai")
@pytest.mark.skipif(os.name == "nt", reason="POSIX file modes")
def test_saved_key_file_is_owner_only(monkeypatch, cli_utils, tmp_path):
def test_saved_key_file_is_owner_only(monkeypatch, prompts, tmp_path):
# The prompt writes a real credential; the file must not be readable by
# other local users whatever the umask is.
old = os.umask(0o002)
try:
_prompt_key(cli_utils, monkeypatch, tmp_path)
_prompt_key(prompts, monkeypatch, tmp_path)
finally:
os.umask(old)
env = tmp_path / ".env"
@@ -173,21 +173,21 @@ def test_saved_key_file_is_owner_only(monkeypatch, cli_utils, tmp_path):
@pytest.mark.skipif(os.name == "nt", reason="POSIX file modes")
def test_existing_key_file_is_tightened_before_writing(monkeypatch, cli_utils, tmp_path):
def test_existing_key_file_is_tightened_before_writing(monkeypatch, prompts, tmp_path):
env = tmp_path / ".env"
env.write_text("OTHER=1\n")
os.chmod(env, 0o664)
_prompt_key(cli_utils, monkeypatch, tmp_path)
_prompt_key(prompts, monkeypatch, tmp_path)
assert stat.S_IMODE(env.stat().st_mode) == 0o600
assert "OTHER=1" in env.read_text()
@pytest.mark.skipif(os.name == "nt", reason="POSIX file modes")
def test_read_only_key_file_is_still_updated(monkeypatch, cli_utils, tmp_path):
def test_read_only_key_file_is_still_updated(monkeypatch, prompts, tmp_path):
env = tmp_path / ".env"
env.write_text("OTHER=1\n")
os.chmod(env, 0o400)
_prompt_key(cli_utils, monkeypatch, tmp_path)
_prompt_key(prompts, monkeypatch, tmp_path)
assert "sk-typed-in" in env.read_text()
assert stat.S_IMODE(env.stat().st_mode) == 0o600
+1 -1
View File
@@ -75,7 +75,7 @@ def test_glm_resolves_to_the_endpoint_its_key_belongs_to():
same platform: glm is Z.AI international (ZHIPU_API_KEY) and glm-cn is
BigModel China. A mismatch sends the key to the other platform and every
call fails auth."""
from cli.utils import resolve_backend_url
from cli.prompts import resolve_backend_url
from tradingagents.llm_clients.api_key_env import get_api_key_env
from tradingagents.llm_clients.openai_client import OPENAI_COMPATIBLE_PROVIDERS
+53 -1
View File
@@ -7,10 +7,16 @@ person to read afterwards. Both got that wrong in ways that hide real content.
from __future__ import annotations
import json
import unittest
import pytest
from cli.main import extract_content_string
from cli.display import (
AnalystWallTimeTracker,
extract_content_string,
sync_analyst_tracker_from_chunk,
)
from tradingagents.graph.analyst_execution import build_analyst_execution_plan
@pytest.mark.unit
@@ -80,3 +86,49 @@ def test_the_live_display_does_not_scroll_the_terminal():
import cli.main as m
assert "screen=True" in inspect.getsource(m.run_analysis)
class AnalystWallTimeTrackerTests(unittest.TestCase):
def test_records_wall_time_when_analyst_completes(self):
plan = build_analyst_execution_plan(["market", "news"])
tracker = AnalystWallTimeTracker(plan)
tracker.mark_started("market", started_at=10.0)
tracker.mark_completed("market", completed_at=13.5)
self.assertEqual(tracker.format_summary(), "Analyst wall time: Market 3.50s")
def test_formats_summary_in_plan_order(self):
plan = build_analyst_execution_plan(["news", "market"])
tracker = AnalystWallTimeTracker(plan)
tracker.mark_started("market", started_at=20.0)
tracker.mark_completed("market", completed_at=22.25)
tracker.mark_started("news", started_at=10.0)
tracker.mark_completed("news", completed_at=14.0)
self.assertEqual(
tracker.format_summary(),
"Analyst wall time: News 4.00s | Market 2.25s",
)
def test_syncs_wall_time_from_sequential_chunks(self):
plan = build_analyst_execution_plan(["market", "news"])
tracker = AnalystWallTimeTracker(plan)
sync_analyst_tracker_from_chunk(tracker, {}, now=10.0)
self.assertEqual(tracker.format_summary(), "Analyst wall time: pending")
sync_analyst_tracker_from_chunk(
tracker,
{"market_report": "done"},
now=13.0,
)
self.assertEqual(tracker.format_summary(), "Analyst wall time: Market 3.00s")
sync_analyst_tracker_from_chunk(
tracker,
{"market_report": "done", "news_report": "done"},
now=18.0,
)
self.assertEqual(tracker.format_summary(), "Analyst wall time: Market 3.00s | News 5.00s")
+3 -3
View File
@@ -15,17 +15,17 @@ import pytest
@pytest.mark.unit
class TestProviderDefaultUrl(unittest.TestCase):
def test_known_providers_resolve(self):
from cli.utils import provider_default_url
from cli.prompts import provider_default_url
self.assertEqual(provider_default_url("openai"), "https://api.openai.com/v1")
self.assertEqual(provider_default_url("DeepSeek"), "https://api.deepseek.com")
self.assertIsNone(provider_default_url("google")) # uses SDK default
def test_unknown_provider_returns_none(self):
from cli.utils import provider_default_url
from cli.prompts import provider_default_url
self.assertIsNone(provider_default_url("not-a-provider"))
def test_ollama_honors_base_url_env(self):
from cli.utils import provider_default_url
from cli.prompts import provider_default_url
with mock.patch.dict(os.environ, {"OLLAMA_BASE_URL": "http://host:1234/v1"}):
self.assertEqual(provider_default_url("ollama"), "http://host:1234/v1")
+2 -2
View File
@@ -134,10 +134,10 @@ def test_selections_are_remembered_after_a_run(monkeypatch):
def test_a_custom_language_is_remembered_without_breaking_the_next_run():
"""A free-text answer is not one of the menu's choices, and questionary
rejects a default it cannot find, so offering it back would crash startup."""
from cli.utils import ask_output_language
from cli.prompts import ask_output_language
save_last_run({"output_language": "Turkish"})
with mock.patch("cli.utils.questionary.select") as select:
with mock.patch("cli.prompts.questionary.select") as select:
select.return_value.ask.return_value = "English"
ask_output_language(load_last_run()["output_language"])
assert select.call_args.kwargs["default"] is None
+1 -1
View File
@@ -6,7 +6,7 @@ stock), #982 (BTC-USDT accepted but unpriceable on Yahoo).
import pytest
from cli.models import AssetType
from cli.utils import detect_asset_type, is_valid_ticker_input, normalize_ticker_symbol
from cli.prompts import detect_asset_type, is_valid_ticker_input, normalize_ticker_symbol
from tradingagents.dataflows.symbols import normalize_symbol
+1 -1
View File
@@ -1,7 +1,7 @@
import unittest
from cli.models import AnalystType, AssetType
from cli.utils import detect_asset_type, filter_analysts_for_asset_type
from cli.prompts import detect_asset_type, filter_analysts_for_asset_type
from tradingagents.graph.propagation import Propagator
+23 -23
View File
@@ -22,7 +22,7 @@ def _console_out(capsys) -> str:
def _resync_reloaded_modules():
"""Restore module state after this file's importlib.reload() calls.
Several tests below reload ``cli.utils`` to re-evaluate OLLAMA_BASE_URL.
Several tests below reload ``cli.prompts`` to re-evaluate OLLAMA_BASE_URL.
That leaves ``cli.main``'s star-imported names (e.g. get_ticker) bound to
the pre-reload module objects, which breaks identity checks in unrelated
tests that happen to run afterward. Re-sync once on teardown so the reload
@@ -30,8 +30,8 @@ def _resync_reloaded_modules():
"""
yield
import cli.main
import cli.utils
importlib.reload(cli.utils)
import cli.prompts
importlib.reload(cli.prompts)
importlib.reload(cli.main)
@@ -98,14 +98,14 @@ def test_explicit_base_url_overrides_env(monkeypatch):
assert "env-set" not in str(llm.openai_api_base)
# ---- cli.utils side: select_llm_provider dropdown -------------------------
# ---- cli.prompts side: select_llm_provider dropdown -------------------------
def test_cli_dropdown_uses_env(monkeypatch):
"""The Ollama entry in the CLI dropdown must reflect OLLAMA_BASE_URL."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://cli-remote:11434/v1")
import cli.utils as cli_utils
importlib.reload(cli_utils)
from cli import prompts
importlib.reload(prompts)
# Reach inside the function via the same env-read it does at call time
ollama_url = (
__import__("os").environ.get("OLLAMA_BASE_URL")
@@ -116,8 +116,8 @@ def test_cli_dropdown_uses_env(monkeypatch):
def test_cli_dropdown_default_when_unset(monkeypatch):
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
import cli.utils as cli_utils
importlib.reload(cli_utils)
from cli import prompts
importlib.reload(prompts)
ollama_url = (
__import__("os").environ.get("OLLAMA_BASE_URL")
or "http://localhost:11434/v1"
@@ -130,9 +130,9 @@ def test_cli_dropdown_default_when_unset(monkeypatch):
def test_confirm_endpoint_shows_default(monkeypatch, capsys):
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
import cli.utils as cli_utils
importlib.reload(cli_utils)
cli_utils.confirm_ollama_endpoint("http://localhost:11434/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://localhost:11434/v1")
out = _console_out(capsys)
assert "http://localhost:11434/v1" in out
assert "OLLAMA_BASE_URL" not in out # not from env
@@ -141,9 +141,9 @@ def test_confirm_endpoint_shows_default(monkeypatch, capsys):
def test_confirm_endpoint_marks_env_origin(monkeypatch, capsys):
monkeypatch.setenv("OLLAMA_BASE_URL", "http://remote-host:11434/v1")
import cli.utils as cli_utils
importlib.reload(cli_utils)
cli_utils.confirm_ollama_endpoint("http://remote-host:11434/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://remote-host:11434/v1")
out = _console_out(capsys)
assert "http://remote-host:11434/v1" in out
assert "OLLAMA_BASE_URL" in out
@@ -152,9 +152,9 @@ def test_confirm_endpoint_marks_env_origin(monkeypatch, capsys):
def test_confirm_endpoint_warns_on_missing_scheme(monkeypatch, capsys):
"""If user sets OLLAMA_BASE_URL=0.0.0.128, advise on the expected shape."""
monkeypatch.setenv("OLLAMA_BASE_URL", "0.0.0.128")
import cli.utils as cli_utils
importlib.reload(cli_utils)
cli_utils.confirm_ollama_endpoint("0.0.0.128")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("0.0.0.128")
out = _console_out(capsys)
assert "missing a scheme" in out
assert "http://<host>:11434/v1" in out
@@ -163,9 +163,9 @@ def test_confirm_endpoint_warns_on_missing_scheme(monkeypatch, capsys):
def test_confirm_endpoint_warns_on_non_default_port_remote(monkeypatch, capsys):
"""A remote host with no :11434 gets a soft hint about port mismatch."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://remote-host/v1")
import cli.utils as cli_utils
importlib.reload(cli_utils)
cli_utils.confirm_ollama_endpoint("http://remote-host/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://remote-host/v1")
out = _console_out(capsys)
assert "port 11434" in out
@@ -173,9 +173,9 @@ def test_confirm_endpoint_warns_on_non_default_port_remote(monkeypatch, capsys):
def test_confirm_endpoint_quiet_on_local_no_port(monkeypatch, capsys):
"""Local host without port shouldn't trigger the remote-port hint."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost/v1")
import cli.utils as cli_utils
importlib.reload(cli_utils)
cli_utils.confirm_ollama_endpoint("http://localhost/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://localhost/v1")
out = _console_out(capsys)
assert "Note" not in out # localhost is fine without explicit port
+1 -1
View File
@@ -68,7 +68,7 @@ def test_any_model_accepted_no_forced_key():
@pytest.mark.unit
def test_env_backend_url_precedence():
# #978: explicit env URL wins over the menu/default regardless of provider source.
from cli.utils import resolve_backend_url
from cli.prompts import resolve_backend_url
assert resolve_backend_url("openai", "https://api.openai.com/v1", env_url="http://proxy/v1") == "http://proxy/v1"
assert resolve_backend_url("openai", "https://api.openai.com/v1", env_url=None) == "https://api.openai.com/v1"
assert resolve_backend_url("deepseek", None, None) == "https://api.deepseek.com"
+25 -25
View File
@@ -6,7 +6,7 @@ from unittest import mock
import pytest
from cli import utils
from cli import prompts
def _asks(value):
@@ -23,10 +23,10 @@ class TestOpenRouterPromptLabel:
captured["message"] = message
return _asks("openrouter/some-model")
with mock.patch.object(utils, "_fetch_openrouter_models",
with mock.patch.object(prompts, "_fetch_openrouter_models",
return_value=[("Some Model", "openrouter/some-model")]), \
mock.patch.object(utils.questionary, "select", side_effect=fake_select):
out = utils.select_openrouter_model(mode)
mock.patch.object(prompts.questionary, "select", side_effect=fake_select):
out = prompts.select_openrouter_model(mode)
assert label in captured["message"]
assert out == "openrouter/some-model"
@@ -44,7 +44,7 @@ class TestOpenRouterLatestFirst:
resp.json.return_value = payload
resp.raise_for_status = mock.Mock()
with mock.patch("requests.get", return_value=resp):
out = utils._fetch_openrouter_models()
out = prompts._fetch_openrouter_models()
assert [mid for _, mid in out] == ["new/model", "mid/model", "old/model"]
@@ -64,9 +64,9 @@ class TestMainstreamFilter:
captured["values"] = [c.value for c in kwargs["choices"]]
return _asks("anthropic/claude-x")
with mock.patch.object(utils, "_fetch_openrouter_models", return_value=models), \
mock.patch.object(utils.questionary, "select", side_effect=fake_select):
utils.select_openrouter_model("quick")
with mock.patch.object(prompts, "_fetch_openrouter_models", return_value=models), \
mock.patch.object(prompts.questionary, "select", side_effect=fake_select):
prompts.select_openrouter_model("quick")
assert "anthropic/claude-x" in captured["values"]
assert "openai/gpt-x" in captured["values"]
@@ -82,9 +82,9 @@ class TestMainstreamFilter:
captured["values"] = [c.value for c in kwargs["choices"]]
return _asks("nex-agi/x")
with mock.patch.object(utils, "_fetch_openrouter_models", return_value=models), \
mock.patch.object(utils.questionary, "select", side_effect=fake_select):
utils.select_openrouter_model("deep")
with mock.patch.object(prompts, "_fetch_openrouter_models", return_value=models), \
mock.patch.object(prompts.questionary, "select", side_effect=fake_select):
prompts.select_openrouter_model("deep")
assert "nex-agi/x" in captured["values"] # fallback keeps the list usable
@@ -92,31 +92,31 @@ class TestMainstreamFilter:
@pytest.mark.unit
class TestCancelExitsCleanly:
def test_dropdown_cancel_exits(self):
with mock.patch.object(utils, "_fetch_openrouter_models", return_value=[]), \
mock.patch.object(utils.questionary, "select", return_value=_asks(None)), \
with mock.patch.object(prompts, "_fetch_openrouter_models", return_value=[]), \
mock.patch.object(prompts.questionary, "select", return_value=_asks(None)), \
pytest.raises(SystemExit):
utils.select_openrouter_model("quick")
prompts.select_openrouter_model("quick")
def test_custom_id_cancel_exits(self):
with mock.patch.object(utils, "_fetch_openrouter_models", return_value=[]), \
mock.patch.object(utils.questionary, "select", return_value=_asks("custom")), \
mock.patch.object(utils.questionary, "text", return_value=_asks(None)), \
with mock.patch.object(prompts, "_fetch_openrouter_models", return_value=[]), \
mock.patch.object(prompts.questionary, "select", return_value=_asks("custom")), \
mock.patch.object(prompts.questionary, "text", return_value=_asks(None)), \
pytest.raises(SystemExit):
utils.select_openrouter_model("deep")
prompts.select_openrouter_model("deep")
def test_prompt_custom_model_id_cancel_exits(self):
with mock.patch.object(utils.questionary, "text", return_value=_asks(None)), \
with mock.patch.object(prompts.questionary, "text", return_value=_asks(None)), \
pytest.raises(SystemExit):
utils._prompt_custom_model_id()
prompts._prompt_custom_model_id()
@pytest.mark.unit
class TestLanguageDefaultsToEnglish:
def test_select_cancel_defaults_english(self):
with mock.patch.object(utils.questionary, "select", return_value=_asks(None)):
assert utils.ask_output_language() == "English"
with mock.patch.object(prompts.questionary, "select", return_value=_asks(None)):
assert prompts.ask_output_language() == "English"
def test_custom_language_cancel_defaults_english(self):
with mock.patch.object(utils.questionary, "select", return_value=_asks("custom")), \
mock.patch.object(utils.questionary, "text", return_value=_asks(None)):
assert utils.ask_output_language() == "English"
with mock.patch.object(prompts.questionary, "select", return_value=_asks("custom")), \
mock.patch.object(prompts.questionary, "text", return_value=_asks(None)):
assert prompts.ask_output_language() == "English"
+4 -4
View File
@@ -2,7 +2,7 @@ import unittest
import pytest
from cli.utils import normalize_ticker_symbol
from cli.prompts import normalize_ticker_symbol
from tradingagents.agents.context import build_instrument_context
@@ -19,10 +19,10 @@ class TickerSymbolHandlingTests(unittest.TestCase):
def test_single_get_ticker_no_shadow(self):
# Regression: cli/main.py had a duplicate get_ticker with an empty
# questionary prompt (rendered as a bare "?") that shadowed the
# descriptive one in cli/utils. Keep a single canonical definition.
# descriptive one in cli/prompts. Keep a single canonical definition.
import cli.main
import cli.utils
self.assertIs(cli.main.get_ticker, cli.utils.get_ticker)
import cli.prompts
self.assertIs(cli.main.get_ticker, cli.prompts.get_ticker)
if __name__ == "__main__":