mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 06:56:39 +03:00
- --ticker, --date and --analysts answer their steps, checked as the prompts check them; --save/--no-save and --show/--no-show answer the questions after the run - with no terminal, the run stops before it starts and names every flag or TRADINGAGENTS_* variable still needed; a missing API key and an announcement no longer wait for input - README shows an unattended run
163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
"""Unattended CLI runs: flags answer the per-run questions, and a run with no
|
|
terminal stops up front, naming what to set, instead of waiting on a prompt."""
|
|
|
|
import datetime
|
|
|
|
import pytest
|
|
import typer
|
|
|
|
from cli import prompts, run, selections
|
|
from cli.models import AnalystType, AssetType
|
|
|
|
UNATTENDED_ENV = {
|
|
"TRADINGAGENTS_OUTPUT_LANGUAGE": "English",
|
|
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "1",
|
|
"TRADINGAGENTS_MAX_RISK_ROUNDS": "1",
|
|
"TRADINGAGENTS_LLM_PROVIDER": "openai",
|
|
"TRADINGAGENTS_QUICK_THINK_LLM": "gpt-6-luna",
|
|
}
|
|
FLAGS = {"ticker": "NVDA", "date": "2026-09-23", "analysts": "market,news", "save": True, "show": False}
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestFlagValues:
|
|
def test_a_ticker_is_normalised_and_an_empty_one_refused(self):
|
|
assert prompts.parse_ticker("0700.hk") == "0700.HK"
|
|
with pytest.raises(ValueError, match="ticker"):
|
|
prompts.parse_ticker(" ")
|
|
with pytest.raises(ValueError, match="ticker"):
|
|
prompts.parse_ticker("NV DA")
|
|
|
|
def test_a_date_must_be_a_past_or_present_day(self):
|
|
assert prompts.parse_analysis_date("2026-09-23") == "2026-09-23"
|
|
tomorrow = (datetime.date.today() + datetime.timedelta(days=1)).isoformat()
|
|
with pytest.raises(ValueError, match="future"):
|
|
prompts.parse_analysis_date(tomorrow)
|
|
with pytest.raises(ValueError, match="YYYY-MM-DD"):
|
|
prompts.parse_analysis_date("23/09/2026")
|
|
|
|
def test_analysts_are_named_and_checked_against_the_asset(self):
|
|
assert prompts.parse_analysts(" Market, news ", AssetType.STOCK) == [AnalystType.MARKET, AnalystType.NEWS]
|
|
with pytest.raises(ValueError, match="market, social, news, fundamentals"):
|
|
prompts.parse_analysts("market,macro", AssetType.STOCK)
|
|
with pytest.raises(ValueError, match="crypto"):
|
|
prompts.parse_analysts("fundamentals", AssetType.CRYPTO)
|
|
with pytest.raises(ValueError, match="at least one"):
|
|
prompts.parse_analysts(" , ", AssetType.STOCK)
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_flags_skip_exactly_their_own_steps(monkeypatch):
|
|
for name, value in UNATTENDED_ENV.items():
|
|
monkeypatch.setenv(name, value)
|
|
|
|
def no_prompt(*a, **k):
|
|
raise AssertionError("prompted although a flag answered the step")
|
|
|
|
for step in ("get_ticker", "get_analysis_date", "select_analysts"):
|
|
monkeypatch.setattr(selections, step, no_prompt)
|
|
monkeypatch.setattr(selections, "fetch_announcements", lambda: None)
|
|
monkeypatch.setattr(selections, "display_announcements", lambda *a: None)
|
|
|
|
chosen = selections._prompt_selections({}, FLAGS)
|
|
|
|
assert chosen["ticker"] == "NVDA" and chosen["analysis_date"] == "2026-09-23"
|
|
assert chosen["analysts"] == [AnalystType.MARKET, AnalystType.NEWS]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_without_a_terminal_every_unanswered_question_is_named_before_the_run(monkeypatch, capsys):
|
|
for name in UNATTENDED_ENV:
|
|
monkeypatch.delenv(name, raising=False)
|
|
monkeypatch.setattr(run.sys.stdin, "isatty", lambda: False)
|
|
|
|
with pytest.raises(typer.Exit):
|
|
run.run_analysis(flags={"ticker": "NVDA"})
|
|
|
|
out = capsys.readouterr().out
|
|
for needed in ("--date", "--analysts", "--save", "--show", "TRADINGAGENTS_LLM_PROVIDER"):
|
|
assert needed in out
|
|
assert "--ticker" not in out
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_nothing_is_missing_when_flags_and_environment_answer_every_step(monkeypatch):
|
|
for name, value in UNATTENDED_ENV.items():
|
|
monkeypatch.setenv(name, value)
|
|
assert selections.unattended_gaps(FLAGS) == []
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_save_and_show_answer_the_questions_after_the_run(monkeypatch, tmp_path):
|
|
def no_prompt(*a, **k):
|
|
raise AssertionError("prompted although --save/--show answered")
|
|
|
|
monkeypatch.setattr(run.typer, "prompt", no_prompt)
|
|
shown = []
|
|
monkeypatch.setattr(run, "display_complete_report", lambda state: shown.append(state))
|
|
graph = type("G", (), {"run_settings": lambda self: {}})()
|
|
state = {"market_report": "M", "final_trade_decision": "**Rating**: Hold"}
|
|
|
|
run._offer_reports(state, graph, {"results_dir": str(tmp_path)}, "NVDA", save=True, show=False)
|
|
|
|
assert list(tmp_path.glob("reports/NVDA_*/complete_report.md"))
|
|
assert shown == []
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_an_announcement_does_not_wait_for_enter_without_a_terminal(monkeypatch):
|
|
from cli import announcements
|
|
from cli.display import console
|
|
|
|
def no_wait(*a):
|
|
raise AssertionError("waited for Enter with no terminal")
|
|
|
|
monkeypatch.setattr(announcements.getpass, "getpass", no_wait)
|
|
monkeypatch.setattr(announcements.sys.stdin, "isatty", lambda: False)
|
|
announcements.display_announcements(console, {"content": "Maintenance tonight", "require_attention": True})
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_a_missing_key_without_a_terminal_names_the_variable(monkeypatch, capsys):
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
monkeypatch.setattr(prompts.sys.stdin, "isatty", lambda: False)
|
|
|
|
with pytest.raises(typer.Exit):
|
|
prompts.ensure_api_key("openai")
|
|
assert "OPENAI_API_KEY" in capsys.readouterr().out
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_command_passes_its_flags_to_the_run(monkeypatch):
|
|
from typer.testing import CliRunner
|
|
|
|
from cli import main as cli_main
|
|
|
|
seen = {}
|
|
monkeypatch.setattr(cli_main, "run_analysis", lambda **kw: seen.update(kw))
|
|
|
|
result = CliRunner().invoke(cli_main.app, ["--ticker", "NVDA", "--date", "2026-09-23",
|
|
"--analysts", "market,news", "--save", "--no-show"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert seen["flags"] == FLAGS
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_sentiment_names_the_sentiment_analyst():
|
|
assert prompts.parse_analysts("sentiment,market", AssetType.STOCK) == [AnalystType.MARKET, AnalystType.SOCIAL]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_an_empty_flag_is_checked_like_its_step_not_reported_missing(monkeypatch):
|
|
assert "--ticker" not in selections.unattended_gaps(dict(FLAGS, ticker=""))
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_a_closed_stdin_counts_as_no_terminal(monkeypatch, capsys):
|
|
monkeypatch.setattr(run.sys, "stdin", None)
|
|
with pytest.raises(typer.Exit):
|
|
run.run_analysis(flags={})
|
|
assert "--ticker" in capsys.readouterr().out
|