refactor(cli): split the interactive choices and the run loop out of cli/main.py

- selections.py asks what to run; run.py builds the graph, streams it to the live view and saves the report
- main.py keeps the Typer app and its two commands
This commit is contained in:
Yijia-Xiao
2026-09-24 05:00:36 +00:00
parent 56bd98f690
commit 4a30cb1c0a
11 changed files with 846 additions and 830 deletions
+15 -15
View File
@@ -10,6 +10,7 @@ from unittest import mock
import pytest
import cli.main as m
import cli.run as cli_run
# Minimal selections dict shaped like get_user_selections()'s return value.
SELECTIONS = {
@@ -28,7 +29,7 @@ SELECTIONS = {
def test_research_depth_sets_both_rounds_without_env(monkeypatch):
for var in ("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "TRADINGAGENTS_MAX_RISK_ROUNDS"):
monkeypatch.delenv(var, raising=False)
cfg = m._build_run_config(SELECTIONS, checkpoint=None)
cfg = cli_run._build_run_config(SELECTIONS, checkpoint=None)
assert cfg["max_debate_rounds"] == 5
assert cfg["max_risk_discuss_rounds"] == 5
@@ -37,9 +38,9 @@ def test_env_round_counts_win_over_selection(monkeypatch):
monkeypatch.setenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "2")
monkeypatch.setenv("TRADINGAGENTS_MAX_RISK_ROUNDS", "4")
# DEFAULT_CONFIG already reflects the env (applied at import); emulate that.
patched = dict(m.DEFAULT_CONFIG, max_debate_rounds=2, max_risk_discuss_rounds=4)
with mock.patch.object(m, "DEFAULT_CONFIG", patched):
cfg = m._build_run_config(SELECTIONS, checkpoint=None)
patched = dict(cli_run.DEFAULT_CONFIG, max_debate_rounds=2, max_risk_discuss_rounds=4)
with mock.patch.object(cli_run, "DEFAULT_CONFIG", patched):
cfg = cli_run._build_run_config(SELECTIONS, checkpoint=None)
assert cfg["max_debate_rounds"] == 2 # env value, not research_depth=5
assert cfg["max_risk_discuss_rounds"] == 4
@@ -47,25 +48,25 @@ def test_env_round_counts_win_over_selection(monkeypatch):
def test_partial_env_only_overrides_that_count(monkeypatch):
monkeypatch.setenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "2")
monkeypatch.delenv("TRADINGAGENTS_MAX_RISK_ROUNDS", raising=False)
patched = dict(m.DEFAULT_CONFIG, max_debate_rounds=2)
with mock.patch.object(m, "DEFAULT_CONFIG", patched):
cfg = m._build_run_config(SELECTIONS, checkpoint=None)
patched = dict(cli_run.DEFAULT_CONFIG, max_debate_rounds=2)
with mock.patch.object(cli_run, "DEFAULT_CONFIG", patched):
cfg = cli_run._build_run_config(SELECTIONS, checkpoint=None)
assert cfg["max_debate_rounds"] == 2 # env wins
assert cfg["max_risk_discuss_rounds"] == 5 # falls through to research_depth
def test_checkpoint_none_preserves_env_default():
patched = dict(m.DEFAULT_CONFIG, checkpoint_enabled=True) # e.g. env-enabled
with mock.patch.object(m, "DEFAULT_CONFIG", patched):
cfg = m._build_run_config(SELECTIONS, checkpoint=None)
patched = dict(cli_run.DEFAULT_CONFIG, checkpoint_enabled=True) # e.g. env-enabled
with mock.patch.object(cli_run, "DEFAULT_CONFIG", patched):
cfg = cli_run._build_run_config(SELECTIONS, checkpoint=None)
assert cfg["checkpoint_enabled"] is True # not clobbered back to False
@pytest.mark.parametrize("flag", [True, False])
def test_checkpoint_flag_overrides_env(flag):
patched = dict(m.DEFAULT_CONFIG, checkpoint_enabled=not flag)
with mock.patch.object(m, "DEFAULT_CONFIG", patched):
cfg = m._build_run_config(SELECTIONS, checkpoint=flag)
patched = dict(cli_run.DEFAULT_CONFIG, checkpoint_enabled=not flag)
with mock.patch.object(cli_run, "DEFAULT_CONFIG", patched):
cfg = cli_run._build_run_config(SELECTIONS, checkpoint=flag)
assert cfg["checkpoint_enabled"] is flag
@@ -89,14 +90,13 @@ def test_glm_resolves_to_the_endpoint_its_key_belongs_to():
def test_a_half_set_round_count_says_which_value_won(capsys, monkeypatch):
"""With only one of the two round-count variables set, the depth prompt is
still shown but half the answer is discarded; the user was never told."""
import cli.main as m
monkeypatch.setenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "1")
monkeypatch.delenv("TRADINGAGENTS_MAX_RISK_ROUNDS", raising=False)
printed = []
monkeypatch.setattr(m.console, "print", lambda *a, **k: printed.append(str(a[0]) if a else ""))
config = m._build_run_config({
config = cli_run._build_run_config({
"ticker": "NVDA", "analysis_date": "2026-09-01", "asset_type": "stock",
"analysts": [], "research_depth": 5, "llm_provider": "openai",
"quick_think_llm": "gpt-5.6-luna", "deep_think_llm": "gpt-5.6",
+9 -8
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import pytest
import cli.run as cli_run
from tradingagents.decision_log import TradingMemoryLog
from tradingagents.graph.trading_graph import TradingAgentsGraph
@@ -145,20 +146,20 @@ def _run_cli(monkeypatch, tmp_path, fake):
from cli.models import AnalystType
buffer = _FakeBuffer()
monkeypatch.setattr(m, "TradingAgentsGraph", lambda *a, **k: fake)
monkeypatch.setattr(m, "message_buffer", buffer)
monkeypatch.setattr(m, "create_layout", lambda: None)
monkeypatch.setattr(m, "update_display", lambda *a, **k: None)
monkeypatch.setattr(m, "Live", _NullLive)
monkeypatch.setattr(m, "get_user_selections", lambda: {
monkeypatch.setattr(cli_run, "TradingAgentsGraph", lambda *a, **k: fake)
monkeypatch.setattr(cli_run, "message_buffer", buffer)
monkeypatch.setattr(cli_run, "create_layout", lambda: None)
monkeypatch.setattr(cli_run, "update_display", lambda *a, **k: None)
monkeypatch.setattr(cli_run, "Live", _NullLive)
monkeypatch.setattr(cli_run, "get_user_selections", lambda: {
"ticker": "NVDA", "analysis_date": "2026-01-10",
"analysts": [AnalystType.MARKET], "asset_type": "stock",
})
monkeypatch.setattr(m, "_build_run_config", lambda selections, checkpoint: {
monkeypatch.setattr(cli_run, "_build_run_config", lambda selections, checkpoint: {
"data_cache_dir": str(tmp_path / "cache"), "results_dir": str(tmp_path / "results"),
})
monkeypatch.setattr(m.typer, "prompt", lambda *a, **k: "N")
m.run_analysis()
cli_run.run_analysis()
return buffer
+46 -47
View File
@@ -11,6 +11,8 @@ from unittest import mock
import pytest
import cli.selections as cli_selections
@pytest.mark.unit
class TestProviderDefaultUrl(unittest.TestCase):
@@ -33,7 +35,6 @@ class TestProviderDefaultUrl(unittest.TestCase):
@pytest.mark.unit
class TestCliSkipsPromptsFromEnv(unittest.TestCase):
def test_env_config_skips_llm_prompts(self):
import cli.main as m
env = {
"TRADINGAGENTS_LLM_PROVIDER": "openai",
@@ -42,7 +43,7 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
"TRADINGAGENTS_LLM_BACKEND_URL": "https://opencode.ai/zen/go/v1",
"TRADINGAGENTS_OUTPUT_LANGUAGE": "Japanese",
}
fake_cfg = dict(m.DEFAULT_CONFIG)
fake_cfg = dict(cli_selections.DEFAULT_CONFIG)
fake_cfg.update({
"llm_provider": "openai",
"backend_url": "https://opencode.ai/zen/go/v1",
@@ -52,19 +53,19 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
})
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \
mock.patch.object(m, "select_research_depth", return_value=1), \
mock.patch.object(m, "ensure_api_key") as ensure_key, \
mock.patch.object(m, "select_llm_provider") as prompt_provider, \
mock.patch.object(m, "ask_output_language") as prompt_lang, \
mock.patch.object(m, "select_shallow_thinking_agent") as prompt_quick, \
mock.patch.object(m, "select_deep_thinking_agent") as prompt_deep:
sel = m.get_user_selections()
mock.patch.object(cli_selections, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(cli_selections, "fetch_announcements", return_value=None), \
mock.patch.object(cli_selections, "display_announcements"), \
mock.patch.object(cli_selections, "get_ticker", return_value="AAPL"), \
mock.patch.object(cli_selections, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(cli_selections, "select_analysts", return_value=[]), \
mock.patch.object(cli_selections, "select_research_depth", return_value=1), \
mock.patch.object(cli_selections, "ensure_api_key") as ensure_key, \
mock.patch.object(cli_selections, "select_llm_provider") as prompt_provider, \
mock.patch.object(cli_selections, "ask_output_language") as prompt_lang, \
mock.patch.object(cli_selections, "select_shallow_thinking_agent") as prompt_quick, \
mock.patch.object(cli_selections, "select_deep_thinking_agent") as prompt_deep:
sel = cli_selections.get_user_selections()
# None of the LLM selection prompts should have been shown.
prompt_provider.assert_not_called()
@@ -85,30 +86,29 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
@pytest.mark.unit
class TestResearchDepthSkippedFromEnv(unittest.TestCase):
def test_both_round_envs_skip_depth_prompt(self):
import cli.main as m
env = {
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "2",
"TRADINGAGENTS_MAX_RISK_ROUNDS": "4",
}
fake_cfg = dict(m.DEFAULT_CONFIG)
fake_cfg = dict(cli_selections.DEFAULT_CONFIG)
fake_cfg.update({"max_debate_rounds": 2, "max_risk_discuss_rounds": 4})
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \
mock.patch.object(m, "select_research_depth") as prompt_depth, \
mock.patch.object(m, "ensure_api_key"), \
mock.patch.object(m, "select_llm_provider", return_value=("openai", None)), \
mock.patch.object(m, "ask_output_language", return_value="English"), \
mock.patch.object(m, "select_shallow_thinking_agent", return_value="gpt-5.4-mini"), \
mock.patch.object(m, "select_deep_thinking_agent", return_value="gpt-5.5"), \
mock.patch.object(m, "ask_openai_reasoning_effort", return_value=None):
sel = m.get_user_selections()
mock.patch.object(cli_selections, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(cli_selections, "fetch_announcements", return_value=None), \
mock.patch.object(cli_selections, "display_announcements"), \
mock.patch.object(cli_selections, "get_ticker", return_value="AAPL"), \
mock.patch.object(cli_selections, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(cli_selections, "select_analysts", return_value=[]), \
mock.patch.object(cli_selections, "select_research_depth") as prompt_depth, \
mock.patch.object(cli_selections, "ensure_api_key"), \
mock.patch.object(cli_selections, "select_llm_provider", return_value=("openai", None)), \
mock.patch.object(cli_selections, "ask_output_language", return_value="English"), \
mock.patch.object(cli_selections, "select_shallow_thinking_agent", return_value="gpt-5.4-mini"), \
mock.patch.object(cli_selections, "select_deep_thinking_agent", return_value="gpt-5.5"), \
mock.patch.object(cli_selections, "ask_openai_reasoning_effort", return_value=None):
sel = cli_selections.get_user_selections()
# The research-depth prompt is skipped; the value comes from the env config.
prompt_depth.assert_not_called()
@@ -118,27 +118,26 @@ class TestResearchDepthSkippedFromEnv(unittest.TestCase):
@pytest.mark.unit
class TestReasoningEffortSkippedFromEnv(unittest.TestCase):
def test_effort_env_skips_step8_prompt(self):
import cli.main as m
env = {"TRADINGAGENTS_OPENAI_REASONING_EFFORT": "high"}
fake_cfg = dict(m.DEFAULT_CONFIG)
fake_cfg = dict(cli_selections.DEFAULT_CONFIG)
fake_cfg.update({"openai_reasoning_effort": "high"})
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \
mock.patch.object(m, "select_research_depth", return_value=1), \
mock.patch.object(m, "ensure_api_key"), \
mock.patch.object(m, "select_llm_provider", return_value=("openai", None)), \
mock.patch.object(m, "ask_output_language", return_value="English"), \
mock.patch.object(m, "select_shallow_thinking_agent", return_value="gpt-5.4-mini"), \
mock.patch.object(m, "select_deep_thinking_agent", return_value="gpt-5.5"), \
mock.patch.object(m, "ask_openai_reasoning_effort") as prompt_effort:
sel = m.get_user_selections()
mock.patch.object(cli_selections, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(cli_selections, "fetch_announcements", return_value=None), \
mock.patch.object(cli_selections, "display_announcements"), \
mock.patch.object(cli_selections, "get_ticker", return_value="AAPL"), \
mock.patch.object(cli_selections, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(cli_selections, "select_analysts", return_value=[]), \
mock.patch.object(cli_selections, "select_research_depth", return_value=1), \
mock.patch.object(cli_selections, "ensure_api_key"), \
mock.patch.object(cli_selections, "select_llm_provider", return_value=("openai", None)), \
mock.patch.object(cli_selections, "ask_output_language", return_value="English"), \
mock.patch.object(cli_selections, "select_shallow_thinking_agent", return_value="gpt-5.4-mini"), \
mock.patch.object(cli_selections, "select_deep_thinking_agent", return_value="gpt-5.5"), \
mock.patch.object(cli_selections, "ask_openai_reasoning_effort") as prompt_effort:
sel = cli_selections.get_user_selections()
# The reasoning-effort prompt is skipped; the value comes from env config.
prompt_effort.assert_not_called()
+26 -26
View File
@@ -13,6 +13,7 @@ from unittest import mock
import pytest
import cli.selections as cli_selections
from cli.models import AnalystType
from cli.prefs import load_last_run, sanitize, save_last_run
@@ -100,26 +101,26 @@ def _answer_every_prompt(monkeypatch):
"""Drive the real selection flow, answering each prompt with a fixed value."""
import cli.main as m
monkeypatch.setattr(m, "fetch_announcements", lambda: [])
monkeypatch.setattr(m, "display_announcements", lambda *a: None)
monkeypatch.setattr(m, "get_ticker", lambda: "NVDA")
monkeypatch.setattr(m, "get_analysis_date", lambda: "2026-09-01")
monkeypatch.setattr(m, "ask_output_language", lambda default=None: "English")
monkeypatch.setattr(m, "select_analysts", lambda asset_type, default=None: [AnalystType.MARKET])
monkeypatch.setattr(m, "select_research_depth", lambda default=None: 3)
monkeypatch.setattr(m, "select_llm_provider", lambda default=None: ("openai", None))
monkeypatch.setattr(m, "select_shallow_thinking_agent", lambda p, default=None: "gpt-5.6-mini")
monkeypatch.setattr(m, "select_deep_thinking_agent", lambda p, default=None: "gpt-5.6")
monkeypatch.setattr(m, "ask_openai_reasoning_effort", lambda: "medium")
monkeypatch.setattr(cli_selections, "fetch_announcements", lambda: [])
monkeypatch.setattr(cli_selections, "display_announcements", lambda *a: None)
monkeypatch.setattr(cli_selections, "get_ticker", lambda: "NVDA")
monkeypatch.setattr(cli_selections, "get_analysis_date", lambda: "2026-09-01")
monkeypatch.setattr(cli_selections, "ask_output_language", lambda default=None: "English")
monkeypatch.setattr(cli_selections, "select_analysts", lambda asset_type, default=None: [AnalystType.MARKET])
monkeypatch.setattr(cli_selections, "select_research_depth", lambda default=None: 3)
monkeypatch.setattr(cli_selections, "select_llm_provider", lambda default=None: ("openai", None))
monkeypatch.setattr(cli_selections, "select_shallow_thinking_agent", lambda p, default=None: "gpt-5.6-mini")
monkeypatch.setattr(cli_selections, "select_deep_thinking_agent", lambda p, default=None: "gpt-5.6")
monkeypatch.setattr(cli_selections, "ask_openai_reasoning_effort", lambda: "medium")
return m
@pytest.mark.unit
def test_selections_are_remembered_after_a_run(monkeypatch):
"""Drives the real flow: a stubbed selections dict would hide a key mismatch."""
m = _answer_every_prompt(monkeypatch)
_answer_every_prompt(monkeypatch)
m.get_user_selections()
cli_selections.get_user_selections()
remembered = load_last_run()
assert remembered["analysts"] == ["market"]
@@ -147,23 +148,22 @@ def test_a_custom_language_is_remembered_without_breaking_the_next_run():
def test_a_remembered_endpoint_is_offered_back(monkeypatch):
"""Users of a local or custom endpoint retyped the URL every run: it was
remembered and validated, then never read."""
import cli.main as m
save_last_run({"llm_provider": "openai_compatible", "backend_url": "http://localhost:1234/v1"})
offered = {}
monkeypatch.setattr(m, "select_llm_provider", lambda default=None: ("openai_compatible", None))
monkeypatch.setattr(m, "prompt_openai_compatible_url",
monkeypatch.setattr(cli_selections, "select_llm_provider", lambda default=None: ("openai_compatible", None))
monkeypatch.setattr(cli_selections, "prompt_openai_compatible_url",
lambda default=None: offered.setdefault("default", default) or "http://x/v1")
monkeypatch.setattr(m, "fetch_announcements", lambda: [])
monkeypatch.setattr(m, "display_announcements", lambda *a: None)
monkeypatch.setattr(m, "get_ticker", lambda: "NVDA")
monkeypatch.setattr(m, "get_analysis_date", lambda: "2026-09-01")
monkeypatch.setattr(m, "ask_output_language", lambda default=None: "English")
monkeypatch.setattr(m, "select_analysts", lambda asset_type, default=None: [AnalystType.MARKET])
monkeypatch.setattr(m, "select_research_depth", lambda default=None: 1)
monkeypatch.setattr(m, "select_shallow_thinking_agent", lambda p, default=None: "local-model")
monkeypatch.setattr(m, "select_deep_thinking_agent", lambda p, default=None: "local-model")
monkeypatch.setattr(cli_selections, "fetch_announcements", lambda: [])
monkeypatch.setattr(cli_selections, "display_announcements", lambda *a: None)
monkeypatch.setattr(cli_selections, "get_ticker", lambda: "NVDA")
monkeypatch.setattr(cli_selections, "get_analysis_date", lambda: "2026-09-01")
monkeypatch.setattr(cli_selections, "ask_output_language", lambda default=None: "English")
monkeypatch.setattr(cli_selections, "select_analysts", lambda asset_type, default=None: [AnalystType.MARKET])
monkeypatch.setattr(cli_selections, "select_research_depth", lambda default=None: 1)
monkeypatch.setattr(cli_selections, "select_shallow_thinking_agent", lambda p, default=None: "local-model")
monkeypatch.setattr(cli_selections, "select_deep_thinking_agent", lambda p, default=None: "local-model")
m.get_user_selections()
cli_selections.get_user_selections()
assert offered["default"] == "http://localhost:1234/v1"
+3 -3
View File
@@ -5,6 +5,7 @@ stock), #982 (BTC-USDT accepted but unpriceable on Yahoo).
"""
import pytest
import cli.run as cli_run
from cli.models import AssetType
from cli.prompts import detect_asset_type, is_valid_ticker_input, normalize_ticker_symbol
from tradingagents.dataflows.symbols import normalize_symbol
@@ -66,10 +67,9 @@ def test_cli_normalize_delegates_to_data_layer():
def test_the_run_directory_cannot_escape_the_results_directory(tmp_path, monkeypatch):
"""Every other path that interpolates a ticker validates it first; the CLI's
own results tree did not, so a ticker of '..' wrote a level up."""
import cli.main as m
with pytest.raises(ValueError):
m._run_directory({"results_dir": str(tmp_path)}, "..", "2026-09-01")
cli_run._run_directory({"results_dir": str(tmp_path)}, "..", "2026-09-01")
ok = m._run_directory({"results_dir": str(tmp_path)}, "NVDA", "2026-09-01")
ok = cli_run._run_directory({"results_dir": str(tmp_path)}, "NVDA", "2026-09-01")
assert str(ok).startswith(str(tmp_path))
+7 -6
View File
@@ -23,16 +23,17 @@ def _resync_reloaded_modules():
"""Restore module state after this file's importlib.reload() calls.
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
doesn't leak across test modules.
That leaves the modules importing from it (cli.selections, then cli.run and
cli.main) bound to the pre-reload functions, which breaks identity checks in
unrelated tests that run afterward. Re-sync them in import order on teardown.
"""
yield
import cli.main
import cli.prompts
importlib.reload(cli.prompts)
importlib.reload(cli.main)
import cli.run
import cli.selections
for module in (cli.prompts, cli.selections, cli.run, cli.main):
importlib.reload(module)
# ---- openai_client side: registry-driven base_url resolution --------------
+9 -8
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import pytest
import cli.run as cli_run
from tradingagents.agents.rating import RATING_REVIEW, extract_rating, parse_rating
INVERTED = ("The aggressive analyst pushed hard for a Buy on the AI backlog, but the "
@@ -145,23 +146,23 @@ def test_the_cli_says_when_a_run_produced_no_usable_rating(monkeypatch, tmp_path
fake = _Graph()
fake.graph = fake
fake.propagator = fake
monkeypatch.setattr(m, "TradingAgentsGraph", lambda *a, **k: fake)
monkeypatch.setattr(m, "create_layout", lambda: None)
monkeypatch.setattr(m, "update_display", lambda *a, **k: None)
monkeypatch.setattr(m, "Live", type("L", (), {"__init__": lambda s, *a, **k: None,
monkeypatch.setattr(cli_run, "TradingAgentsGraph", lambda *a, **k: fake)
monkeypatch.setattr(cli_run, "create_layout", lambda: None)
monkeypatch.setattr(cli_run, "update_display", lambda *a, **k: None)
monkeypatch.setattr(cli_run, "Live", type("L", (), {"__init__": lambda s, *a, **k: None,
"__enter__": lambda s: s,
"__exit__": lambda s, *a: False}))
monkeypatch.setattr(m.console, "print", lambda *a, **k: printed.append(" ".join(str(x) for x in a)))
monkeypatch.setattr(m, "display_complete_report", lambda *a, **k: None)
monkeypatch.setattr(cli_run, "display_complete_report", lambda *a, **k: None)
monkeypatch.setattr(m.typer, "prompt", lambda *a, **k: "N")
monkeypatch.setattr(m, "get_user_selections", lambda: {
monkeypatch.setattr(cli_run, "get_user_selections", lambda: {
"ticker": "NVDA", "analysis_date": "2026-01-10",
"analysts": [AnalystType.MARKET], "asset_type": "stock",
})
monkeypatch.setattr(m, "_build_run_config", lambda s, c: {
monkeypatch.setattr(cli_run, "_build_run_config", lambda s, c: {
"data_cache_dir": str(tmp_path / "c"), "results_dir": str(tmp_path / "r")})
m.run_analysis()
cli_run.run_analysis()
assert any("review" in line.lower() for line in printed), printed[-5:]
+4 -5
View File
@@ -17,12 +17,11 @@ class TickerSymbolHandlingTests(unittest.TestCase):
self.assertIn("exchange suffix", context)
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/prompts. Keep a single canonical definition.
import cli.main
# A second get_ticker with an empty prompt (a bare "?") once shadowed
# the descriptive one; the selection flow must use the one in prompts.
import cli.prompts
self.assertIs(cli.main.get_ticker, cli.prompts.get_ticker)
import cli.selections
self.assertIs(cli.selections.get_ticker, cli.prompts.get_ticker)
if __name__ == "__main__":