mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
fix(cli): finish the run surface
- backtest takes --run-id, so an interrupted sweep continues - an inverted date range or an empty ticker list is an error, not a clean zero-cell run - a setup failure in a sweep reports one line instead of a traceback - the remembered endpoint URL is offered back - a round count set in the environment says it overrode the chosen research depth - the run directory validates the ticker, and the report saves under results_dir - the run says whether it resumed a saved run or started fresh
This commit is contained in:
@@ -89,3 +89,61 @@ def test_every_command_is_registered_when_run_as_a_module():
|
||||
capture_output=True, text=True, timeout=120)
|
||||
assert out.returncode == 0, out.stderr[-400:]
|
||||
assert "--start" in out.stdout
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_cli_says_whether_a_run_resumed(monkeypatch):
|
||||
"""The README promises the user can tell a resumed run from a fresh one.
|
||||
The graph logs it, but nothing configures logging, so it was never shown."""
|
||||
import cli.main as m
|
||||
|
||||
messages = []
|
||||
monkeypatch.setattr(m.message_buffer, "add_message",
|
||||
lambda kind, text: messages.append(text), raising=False)
|
||||
|
||||
m._announce_checkpoint_state(type("G", (), {"_resuming": True})(), "NVDA", "2026-01-10")
|
||||
m._announce_checkpoint_state(type("G", (), {"_resuming": False})(), "NVDA", "2026-01-10")
|
||||
|
||||
assert any("resum" in text.lower() for text in messages)
|
||||
assert any("fresh" in text.lower() for text in messages)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_backtest_can_continue_an_interrupted_sweep(runner, monkeypatch, tmp_path):
|
||||
"""Resuming is what makes a long sweep practical, and the Python API has it."""
|
||||
swept = []
|
||||
monkeypatch.setattr(m, "run_backtest", lambda *a, **kw: swept.append(kw) or _Result(tmp_path))
|
||||
monkeypatch.setattr(m, "summarize", lambda log: _Summary())
|
||||
|
||||
result = runner.invoke(m.app, ["backtest", "NVDA", "--start", "2026-06-01",
|
||||
"--end", "2026-06-08", "--run-id", "20260617_120000"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert swept[0]["run_id"] == "20260617_120000"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("args, expected", [
|
||||
(["backtest", "NVDA", "--start", "2026-08-01", "--end", "2026-06-08"], "before"),
|
||||
(["backtest", ",,", "--start", "2026-06-01", "--end", "2026-06-08"], "ticker"),
|
||||
])
|
||||
def test_backtest_rejects_input_that_would_sweep_nothing(runner, args, expected):
|
||||
"""An inverted range or an empty ticker list reported a clean zero-cell run,
|
||||
which reads as 'nothing to find' rather than 'you asked for nothing'."""
|
||||
result = runner.invoke(m.app, args)
|
||||
assert result.exit_code == 1
|
||||
assert expected in result.output.lower()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_backtest_reports_a_setup_failure_in_one_line(runner, monkeypatch):
|
||||
"""A missing key or a bad analyst name produced a raw traceback."""
|
||||
def _explode(*a, **kw):
|
||||
raise ValueError("API key for provider 'openai' is not set")
|
||||
|
||||
monkeypatch.setattr(m, "run_backtest", _explode)
|
||||
result = runner.invoke(m.app, ["backtest", "NVDA", "--start", "2026-06-01", "--end", "2026-06-08"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "API key" in result.output
|
||||
assert "Traceback" not in result.output
|
||||
|
||||
@@ -83,3 +83,25 @@ def test_glm_resolves_to_the_endpoint_its_key_belongs_to():
|
||||
assert get_api_key_env("glm") == "ZHIPU_API_KEY"
|
||||
assert "z.ai" in OPENAI_COMPATIBLE_PROVIDERS["glm"].base_url
|
||||
assert "bigmodel.cn" in OPENAI_COMPATIBLE_PROVIDERS["glm-cn"].base_url
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
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({
|
||||
"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",
|
||||
"backend_url": None, "output_language": "English",
|
||||
}, None)
|
||||
|
||||
assert config["max_risk_discuss_rounds"] == 5
|
||||
assert any("TRADINGAGENTS_MAX_DEBATE_ROUNDS" in line for line in printed), printed
|
||||
|
||||
@@ -141,3 +141,29 @@ def test_a_custom_language_is_remembered_without_breaking_the_next_run():
|
||||
select.return_value.ask.return_value = "English"
|
||||
ask_output_language(load_last_run()["output_language"])
|
||||
assert select.call_args.kwargs["default"] is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
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",
|
||||
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")
|
||||
|
||||
m.get_user_selections()
|
||||
|
||||
assert offered["default"] == "http://localhost:1234/v1"
|
||||
|
||||
@@ -60,3 +60,16 @@ def test_cli_normalize_delegates_to_data_layer():
|
||||
# CLI must produce the same canonical symbol the data path will price.
|
||||
for raw in ("XAUUSD", "BTCUSD", "btc-usdt", "AAPL"):
|
||||
assert normalize_ticker_symbol(raw) == normalize_symbol(raw)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
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")
|
||||
|
||||
ok = m._run_directory({"results_dir": str(tmp_path)}, "NVDA", "2026-09-01")
|
||||
assert str(ok).startswith(str(tmp_path))
|
||||
|
||||
Reference in New Issue
Block a user