feat(cli): run an analysis without questions from flags (#1127, #1133)

- --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
This commit is contained in:
Yijia-Xiao
2026-09-24 20:42:51 +00:00
parent a1b3b5bab8
commit 0c602846ba
11 changed files with 347 additions and 59 deletions
+8
View File
@@ -201,6 +201,14 @@ python -m cli.main # alternative: run directly from source
``` ```
You will see a screen where you can select your desired tickers, analysis date, LLM provider, research depth, and more. Your previous run's answers come back as the defaults, so pressing Enter accepts them. The `TRADINGAGENTS_*` variables in `.env` still skip their step entirely. You will see a screen where you can select your desired tickers, analysis date, LLM provider, research depth, and more. Your previous run's answers come back as the defaults, so pressing Enter accepts them. The `TRADINGAGENTS_*` variables in `.env` still skip their step entirely.
To run without questions, for a scheduled job or a script, answer the per-run steps with flags and the rest with `TRADINGAGENTS_*` variables:
```bash
export TRADINGAGENTS_LLM_PROVIDER=openai TRADINGAGENTS_QUICK_THINK_LLM=gpt-6-luna TRADINGAGENTS_DEEP_THINK_LLM=gpt-6-sol
export TRADINGAGENTS_OUTPUT_LANGUAGE=English TRADINGAGENTS_MAX_DEBATE_ROUNDS=1 TRADINGAGENTS_MAX_RISK_ROUNDS=1
tradingagents --ticker NVDA --date 2026-09-23 --analysts market,news,fundamentals --save --no-show
```
Each flag skips only its own question. Run without a terminal, a missing answer stops the run before it starts and names the flag or variable to set.
### Markets and tickers ### Markets and tickers
TradingAgents works with any market Yahoo Finance covers, using the exchange-suffixed ticker. Company identity and the alpha benchmark resolve automatically per market. TradingAgents works with any market Yahoo Finance covers, using the exchange-suffixed ticker. Company identity and the alpha benchmark resolve automatically per market.
+2 -1
View File
@@ -1,4 +1,5 @@
import getpass import getpass
import sys
import requests import requests
from rich.console import Console from rich.console import Console
@@ -46,7 +47,7 @@ def display_announcements(console: Console, data: dict) -> None:
) )
console.print(panel) console.print(panel)
if require_attention: if require_attention and sys.stdin.isatty():
getpass.getpass("Press Enter to continue...") getpass.getpass("Press Enter to continue...")
else: else:
console.print() console.print()
+18 -2
View File
@@ -47,8 +47,23 @@ def analyze(
help="JSON file with current holdings and cash, so the trader, risk and " help="JSON file with current holdings and cash, so the trader, risk and "
"portfolio agents size against your actual position.", "portfolio agents size against your actual position.",
), ),
ticker: str = typer.Option(None, "--ticker", help="Ticker to analyze, e.g. NVDA or 0700.HK; skips the prompt"),
date: str = typer.Option(None, "--date", help="Analysis date, YYYY-MM-DD; skips the prompt"),
analysts: str = typer.Option(
None, "--analysts", help="Comma-separated analysts, e.g. market,news; skips the prompt"
),
save: bool | None = typer.Option(
None, "--save/--no-save", help="Save the report under results_dir without asking"
),
show: bool | None = typer.Option(
None, "--show/--no-show", help="Show the full report at the end without asking"
),
): ):
"""Run an analysis. This is what a bare `tradingagents` does.""" """Run an analysis. This is what a bare `tradingagents` does.
Flags answer their questions; with provider, models, depth and language also
set through TRADINGAGENTS_* variables, the run asks nothing.
"""
if ctx.invoked_subcommand is not None: if ctx.invoked_subcommand is not None:
return return
if clear_checkpoints: if clear_checkpoints:
@@ -64,7 +79,8 @@ def analyze(
raise typer.Exit(code=1) from None raise typer.Exit(code=1) from None
try: try:
run_analysis(checkpoint=checkpoint, portfolio=portfolio_context) flags = {"ticker": ticker, "date": date, "analysts": analysts, "save": save, "show": show}
run_analysis(checkpoint=checkpoint, portfolio=portfolio_context, flags=flags)
except _NO_CONSOLE_ERRORS: except _NO_CONSOLE_ERRORS:
# A terminal with no console buffer cannot host the interactive prompts. # A terminal with no console buffer cannot host the interactive prompts.
# Emit one actionable line on stderr instead of a prompt_toolkit # Emit one actionable line on stderr instead of a prompt_toolkit
+42
View File
@@ -1,7 +1,10 @@
import datetime
import os import os
import sys
from pathlib import Path from pathlib import Path
import questionary import questionary
import typer
from dotenv import find_dotenv, set_key from dotenv import find_dotenv, set_key
from cli.display import console from cli.display import console
@@ -60,6 +63,41 @@ def get_ticker() -> str:
return normalize_ticker_symbol(ticker) if ticker.strip() else "SPY" return normalize_ticker_symbol(ticker) if ticker.strip() else "SPY"
def parse_ticker(value: str) -> str:
"""A ticker given on the command line, canonical; empty or malformed is refused."""
if not value.strip() or not is_valid_ticker_input(value):
raise ValueError(f"not a ticker symbol: {value!r} (e.g. {TICKER_INPUT_EXAMPLES})")
return normalize_ticker_symbol(value)
def parse_analysis_date(value: str) -> str:
"""An analysis date as YYYY-MM-DD, today or earlier."""
try:
day = datetime.datetime.strptime(value.strip(), "%Y-%m-%d").date()
except ValueError:
raise ValueError(f"not a date: {value!r}; use YYYY-MM-DD") from None
if day > datetime.date.today():
raise ValueError(f"{value} is in the future")
return day.isoformat()
def parse_analysts(value: str, asset_type: AssetType) -> list[AnalystType]:
"""Comma-separated analyst names, in the canonical order, checked against the asset."""
# "sentiment" is the name users see; the analyst's key is "social".
names = [{"sentiment": "social"}.get(n, n) for n in (n.strip().lower() for n in value.split(",")) if n]
if not names:
raise ValueError("name at least one analyst")
known = {a.value: a for a in AnalystType}
unknown = [n for n in names if n not in known]
if unknown:
raise ValueError(f"unknown analyst {', '.join(unknown)}; choose from {', '.join(known)}")
available = filter_analysts_for_asset_type(list(known.values()), asset_type)
unavailable = [n for n in names if known[n] not in available]
if unavailable:
raise ValueError(f"{', '.join(unavailable)} is not available for {asset_type.value}")
return [a for a in available if a.value in names]
def normalize_ticker_symbol(ticker: str) -> str: def normalize_ticker_symbol(ticker: str) -> str:
"""Resolve user input to its canonical Yahoo symbol (single source of truth). """Resolve user input to its canonical Yahoo symbol (single source of truth).
@@ -611,6 +649,10 @@ def ensure_api_key(provider: str) -> str | None:
if existing: if existing:
return existing return existing
if not sys.stdin.isatty():
console.print(f"[red]{env_var} is not set; there is no terminal to ask for it.[/red]")
raise typer.Exit(code=1)
console.print( console.print(
f"\n[yellow]{env_var} is not set in your environment.[/yellow]" f"\n[yellow]{env_var} is not set in your environment.[/yellow]"
) )
+35 -18
View File
@@ -2,6 +2,7 @@
import datetime import datetime
import os import os
import sys
import time import time
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
@@ -21,7 +22,7 @@ from cli.display import (
update_display, update_display,
update_research_team_status, update_research_team_status,
) )
from cli.selections import get_user_selections from cli.selections import get_user_selections, unattended_gaps
from cli.stats_handler import StatsCallbackHandler from cli.stats_handler import StatsCallbackHandler
from tradingagents.agents.rating import is_review, run_rating from tradingagents.agents.rating import is_review, run_rating
from tradingagents.dataflows.symbols import safe_ticker_component from tradingagents.dataflows.symbols import safe_ticker_component
@@ -93,9 +94,19 @@ def _build_run_config(selections: dict, checkpoint: bool | None) -> dict:
return config return config
def run_analysis(checkpoint: bool | None = None, portfolio=None): def run_analysis(checkpoint: bool | None = None, portfolio=None, flags=None):
# First get all user selections flags = flags or {}
selections = get_user_selections() # With no terminal nothing can answer a prompt: name every question still
# open before any model is called, rather than stopping at the first one.
if not (sys.stdin and sys.stdin.isatty()):
gaps = unattended_gaps(flags)
if gaps:
console.print("[red]No terminal to answer the setup questions. Set:[/red]")
for gap in gaps:
console.print(f" {gap}")
raise typer.Exit(code=1)
selections = get_user_selections(flags)
config = _build_run_config(selections, checkpoint) config = _build_run_config(selections, checkpoint)
@@ -366,29 +377,35 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
) )
console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]") console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]")
# Prompt to save report _offer_reports(final_state, graph, config, selections["ticker"],
save_choice = typer.prompt("Save report?", default="Y").strip().upper() save=flags.get("save"), show=flags.get("show"))
if save_choice in ("Y", "YES", ""):
def _offer_reports(final_state, graph, config, ticker, save=None, show=None):
"""Save the report tree and show it; ``save``/``show`` answer the questions when given."""
asked = save is None
if asked:
save = typer.prompt("Save report?", default="Y").strip().upper() in ("Y", "YES", "")
if save:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# Under results_dir, not the working directory: in Docker the working # Under results_dir, not the working directory: in Docker the working
# directory is inside the container and the report goes with it, while # directory is inside the container and the report goes with it, while
# results_dir is the mounted volume the rest of the run already writes to. # results_dir is the mounted volume the rest of the run already writes to.
default_path = (Path(config["results_dir"]) / "reports" save_path = (Path(config["results_dir"]) / "reports"
/ f"{safe_ticker_component(selections['ticker'])}_{timestamp}") / f"{safe_ticker_component(ticker)}_{timestamp}")
save_path_str = typer.prompt( if asked: # someone at the prompt may pick another folder
"Save path (press Enter for default)", save_path = Path(typer.prompt(
default=str(default_path) "Save path (press Enter for default)", default=str(save_path)
).strip() ).strip())
save_path = Path(save_path_str)
try: try:
report_file = write_report_tree(final_state, selections["ticker"], save_path, report_file = write_report_tree(final_state, ticker, save_path,
settings=graph.run_settings()) settings=graph.run_settings())
console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}") console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}")
console.print(f" [dim]Complete report:[/dim] {report_file.name}") console.print(f" [dim]Complete report:[/dim] {report_file.name}")
except Exception as e: except Exception as e:
console.print(f"[red]Error saving report: {e}[/red]") console.print(f"[red]Error saving report: {e}[/red]")
# Prompt to display full report if show is None:
display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper() show = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper() in ("Y", "YES", "")
if display_choice in ("Y", "YES", ""): if show:
display_complete_report(final_state) display_complete_report(final_state)
+47 -15
View File
@@ -25,6 +25,9 @@ from cli.prompts import (
detect_asset_type, detect_asset_type,
ensure_api_key, ensure_api_key,
get_ticker, get_ticker,
parse_analysis_date,
parse_analysts,
parse_ticker,
prompt_openai_compatible_url, prompt_openai_compatible_url,
resolve_backend_url, resolve_backend_url,
select_analysts, select_analysts,
@@ -36,15 +39,40 @@ from cli.prompts import (
from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.default_config import DEFAULT_CONFIG
def get_user_selections(): def get_user_selections(flags=None):
"""Ask for the run's settings, offering the previous run's answers.""" """Ask for the run's settings, offering the previous run's answers."""
selections = _prompt_selections(load_last_run()) selections = _prompt_selections(load_last_run(), flags or {})
save_last_run(selections) save_last_run(selections)
return selections return selections
def _prompt_selections(prefs): def unattended_gaps(flags) -> list[str]:
"""Walk the selection steps. ``prefs`` prefills, the environment skips.""" """The flags and environment variables a run with no terminal still needs."""
env = os.environ.get
gaps = [f"--{name}" for name in ("ticker", "date", "analysts") if flags.get(name) is None]
gaps += [f"--{name} or --no-{name}" for name in ("save", "show") if flags.get(name) is None]
if not env("TRADINGAGENTS_OUTPUT_LANGUAGE"):
gaps.append("TRADINGAGENTS_OUTPUT_LANGUAGE")
if not (env("TRADINGAGENTS_MAX_DEBATE_ROUNDS") and env("TRADINGAGENTS_MAX_RISK_ROUNDS")):
gaps.append("TRADINGAGENTS_MAX_DEBATE_ROUNDS and TRADINGAGENTS_MAX_RISK_ROUNDS")
if not env("TRADINGAGENTS_LLM_PROVIDER"):
gaps.append("TRADINGAGENTS_LLM_PROVIDER")
if not (env("TRADINGAGENTS_QUICK_THINK_LLM") or env("TRADINGAGENTS_DEEP_THINK_LLM")):
gaps.append("TRADINGAGENTS_QUICK_THINK_LLM or TRADINGAGENTS_DEEP_THINK_LLM")
return gaps
def _from_flag(parse, value, *args):
"""A flag's value through the same check its prompt applies; a bad one ends the run."""
try:
return parse(value, *args)
except ValueError as exc:
console.print(f"[red]{exc}[/red]")
raise typer.Exit(code=1) from None
def _prompt_selections(prefs, flags):
"""Walk the selection steps. ``prefs`` prefills; flags and the environment skip."""
with open(Path(__file__).parent / "static" / "welcome.txt", encoding="utf-8") as f: with open(Path(__file__).parent / "static" / "welcome.txt", encoding="utf-8") as f:
welcome_ascii = f.read() welcome_ascii = f.read()
@@ -93,6 +121,10 @@ def _prompt_selections(prefs):
return prompt_fn() return prompt_fn()
# Step 1: Ticker symbol # Step 1: Ticker symbol
if flags.get("ticker") is not None:
selected_ticker = _from_flag(parse_ticker, flags["ticker"])
console.print(f"[green]✓ Ticker from --ticker:[/green] {selected_ticker}")
else:
console.print( console.print(
create_question_box( create_question_box(
"Step 1: Ticker Symbol", "Step 1: Ticker Symbol",
@@ -110,6 +142,10 @@ def _prompt_selections(prefs):
) )
# Step 2: Analysis date # Step 2: Analysis date
if flags.get("date") is not None:
analysis_date = _from_flag(parse_analysis_date, flags["date"])
console.print(f"[green]✓ Analysis date from --date:[/green] {analysis_date}")
else:
default_date = datetime.datetime.now().strftime("%Y-%m-%d") default_date = datetime.datetime.now().strftime("%Y-%m-%d")
console.print( console.print(
create_question_box( create_question_box(
@@ -136,12 +172,15 @@ def _prompt_selections(prefs):
output_language = ask_output_language(prefs.get("output_language")) output_language = ask_output_language(prefs.get("output_language"))
# Step 4: Select analysts # Step 4: Select analysts
prefs = sanitize(prefs, asset_type.value)
if flags.get("analysts") is not None:
selected_analysts = _from_flag(parse_analysts, flags["analysts"], asset_type)
else:
console.print( console.print(
create_question_box( create_question_box(
"Step 4: Analysts Team", "Select your LLM analyst agents for the analysis" "Step 4: Analysts Team", "Select your LLM analyst agents for the analysis"
) )
) )
prefs = sanitize(prefs, asset_type.value)
selected_analysts = select_analysts(asset_type, prefs.get("analysts")) selected_analysts = select_analysts(asset_type, prefs.get("analysts"))
console.print( console.print(
f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}" f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}"
@@ -303,13 +342,6 @@ def get_analysis_date():
"", default=datetime.datetime.now().strftime("%Y-%m-%d") "", default=datetime.datetime.now().strftime("%Y-%m-%d")
) )
try: try:
# Validate date format and ensure it's not in the future return parse_analysis_date(date_str)
analysis_date = datetime.datetime.strptime(date_str, "%Y-%m-%d") except ValueError as exc:
if analysis_date.date() > datetime.datetime.now().date(): console.print(f"[red]Error: {exc}[/red]")
console.print("[red]Error: Analysis date cannot be in the future[/red]")
continue
return date_str
except ValueError:
console.print(
"[red]Error: Invalid date format. Please use YYYY-MM-DD[/red]"
)
+9
View File
@@ -45,6 +45,15 @@ def _no_network(request, monkeypatch):
monkeypatch.setattr(socket.socket, "connect_ex", refuse) monkeypatch.setattr(socket.socket, "connect_ex", refuse)
@pytest.fixture(autouse=True)
def _at_a_terminal(monkeypatch):
"""Tests of the interactive steps run as if at a terminal; pytest's stdin is
not one. A test of an unattended run sets isatty to False itself."""
import sys
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _own_cli_prefs(tmp_path, monkeypatch): def _own_cli_prefs(tmp_path, monkeypatch):
"""The CLI keeps the last run's selections in the user's home; tests keep theirs apart.""" """The CLI keeps the last run's selections in the user's home; tests keep theirs apart."""
+2 -1
View File
@@ -25,7 +25,8 @@ calls: list = []
@pytest.mark.unit @pytest.mark.unit
def test_no_arguments_still_runs_an_analysis(runner): def test_no_arguments_still_runs_an_analysis(runner):
assert runner.invoke(m.app, []).exit_code == 0 assert runner.invoke(m.app, []).exit_code == 0
assert calls == [("analysis", {"checkpoint": None, "portfolio": None})] no_flags = {"ticker": None, "date": None, "analysts": None, "save": None, "show": None}
assert calls == [("analysis", {"checkpoint": None, "portfolio": None, "flags": no_flags})]
@pytest.mark.unit @pytest.mark.unit
+162
View File
@@ -0,0 +1,162 @@
"""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
+1 -1
View File
@@ -147,7 +147,7 @@ def _run_cli(monkeypatch, tmp_path, fake):
monkeypatch.setattr(cli_run, "create_layout", lambda: None) monkeypatch.setattr(cli_run, "create_layout", lambda: None)
monkeypatch.setattr(cli_run, "update_display", lambda *a, **k: None) monkeypatch.setattr(cli_run, "update_display", lambda *a, **k: None)
monkeypatch.setattr(cli_run, "Live", _NullLive) monkeypatch.setattr(cli_run, "Live", _NullLive)
monkeypatch.setattr(cli_run, "get_user_selections", lambda: { monkeypatch.setattr(cli_run, "get_user_selections", lambda flags=None: {
"ticker": "NVDA", "analysis_date": "2026-01-10", "ticker": "NVDA", "analysis_date": "2026-01-10",
"analysts": [AnalystType.MARKET], "asset_type": "stock", "analysts": [AnalystType.MARKET], "asset_type": "stock",
}) })
+1 -1
View File
@@ -151,7 +151,7 @@ def test_the_cli_says_when_a_run_produced_no_usable_rating(monkeypatch, tmp_path
monkeypatch.setattr(m.console, "print", lambda *a, **k: printed.append(" ".join(str(x) for x in a))) monkeypatch.setattr(m.console, "print", lambda *a, **k: printed.append(" ".join(str(x) for x in a)))
monkeypatch.setattr(cli_run, "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.typer, "prompt", lambda *a, **k: "N")
monkeypatch.setattr(cli_run, "get_user_selections", lambda: { monkeypatch.setattr(cli_run, "get_user_selections", lambda flags=None: {
"ticker": "NVDA", "analysis_date": "2026-01-10", "ticker": "NVDA", "analysis_date": "2026-01-10",
"analysts": [AnalystType.MARKET], "asset_type": "stock", "analysts": [AnalystType.MARKET], "asset_type": "stock",
}) })