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
+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()