mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 06:56:39 +03:00
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:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 --------------
|
||||
|
||||
@@ -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:]
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user