mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-04 00:22:43 +02:00
- a saved report also includes complete_report.html: the report as one self-contained page with its sections listed beside the text, readable in a browser, on a phone and in print - answering the save question at the prompt also asks about the page and offers to open it in a local browser window; --no-html and save_reports(..., html=False) skip it - the Tauric Research logo leads the header at the top left, and a line at the foot credits TradingAgents and Tauric Research - the page loads nothing and runs nothing (inline styles, a content-security policy); report text is rendered without raw HTML or images - markdown-it-py is declared as a dependency
175 lines
8.2 KiB
Python
175 lines
8.2 KiB
Python
"""An optional HTML copy of the report, for readers who do not open Markdown (#1419).
|
|
|
|
It is one self-contained file: no script, no remote asset, nothing a report's
|
|
text can inject; the Markdown report is unchanged.
|
|
"""
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from tradingagents.reporting import write_report_tree
|
|
|
|
SETTINGS = {"version": "1.2.3", "llm_provider": "openai", "deep_think_llm": "gpt-6-sol",
|
|
"quick_think_llm": "gpt-6-luna", "analysts": ["market", "news"], "max_debate_rounds": 1,
|
|
"max_risk_discuss_rounds": 1, "data_vendors": {"core_stock_apis": "yfinance"}}
|
|
|
|
|
|
def _state(**overrides):
|
|
state = {"trade_date": "2026-09-26",
|
|
"market_report": "# Market view\n\nPrice | Level\n--- | ---\nClose | 181.2\n",
|
|
"news_report": "News text.",
|
|
"final_trade_decision": "**Rating**: Overweight\n\nAdd on weakness."}
|
|
state.update(overrides)
|
|
return state
|
|
|
|
|
|
def _html(tmp_path, **overrides):
|
|
write_report_tree(_state(**overrides), "NVDA", tmp_path, settings=SETTINGS, html=True)
|
|
return (tmp_path / "complete_report.html").read_text(encoding="utf-8")
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_html_page_is_written_unless_declined(tmp_path):
|
|
write_report_tree(_state(), "NVDA", tmp_path / "plain", settings=SETTINGS, html=False)
|
|
assert not (tmp_path / "plain" / "complete_report.html").exists()
|
|
write_report_tree(_state(), "NVDA", tmp_path / "default", settings=SETTINGS)
|
|
assert (tmp_path / "default" / "complete_report.html").exists()
|
|
|
|
page = _html(tmp_path / "html")
|
|
assert "<h1" in page and "NVDA" in page and "Overweight" in page and "2026-09-26" in page
|
|
assert '<table>' in page and "181.2" in page
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_markdown_report_is_the_same_with_or_without_html(tmp_path):
|
|
write_report_tree(_state(), "NVDA", tmp_path / "a", settings=SETTINGS, html=False)
|
|
write_report_tree(_state(), "NVDA", tmp_path / "b", settings=SETTINGS)
|
|
def undated(path):
|
|
return re.sub(r"- Generated: .*\n", "", path.read_text(encoding="utf-8"))
|
|
|
|
assert undated(tmp_path / "a" / "complete_report.md") == undated(tmp_path / "b" / "complete_report.md")
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_page_loads_nothing_and_runs_nothing(tmp_path):
|
|
page = _html(tmp_path)
|
|
assert "Content-Security-Policy" in page and "default-src 'none'" in page
|
|
assert "<script" not in page.lower()
|
|
assert not re.search(r'<(img|link|iframe|object|embed)\b', page, re.IGNORECASE)
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_report_text_cannot_inject_markup_or_fetch_anything(tmp_path):
|
|
hostile = ("<script>alert(1)</script> <img src=x onerror=alert(1)>\n\n"
|
|
" [x](javascript:alert(1))")
|
|
page = _html(tmp_path, news_report=hostile)
|
|
assert "<script>alert" not in page and "<img" not in page.lower()
|
|
assert "tracker.example/p.png" not in re.findall(r'(?:src|href)="([^"]*)"', page)
|
|
assert 'href="javascript:' not in page
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_header_values_are_escaped(tmp_path):
|
|
write_report_tree(_state(), "<b>NV</b>", tmp_path, settings={**SETTINGS, "deep_think_llm": "<i>m</i>"}, html=True)
|
|
page = (tmp_path / "complete_report.html").read_text(encoding="utf-8")
|
|
assert "<b>NV</b>" not in page and "<b>NV</b>" in page and "<i>m</i>" not in page
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_an_agent_s_own_headings_sit_below_its_heading(tmp_path):
|
|
page = _html(tmp_path)
|
|
assert page.count("<h1") == 1 # only the report's title
|
|
assert re.search(r"<h4[^>]*>Market view</h4>", page)
|
|
|
|
|
|
def _fields(fragment):
|
|
return re.findall(r'<span class="field">([^<]*)</span>', fragment)
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_run_details_show_each_value_as_its_own_field(tmp_path):
|
|
settings = {**SETTINGS, "deep_think_provider": "anthropic", "deep_think_llm": "claude-opus-5-5",
|
|
"data_vendors": {"core_stock_apis": "yfinance", "fundamental_data": "sec_edgar,yfinance"}}
|
|
write_report_tree(_state(), "NVDA", tmp_path, settings=settings, html=True)
|
|
page = (tmp_path / "complete_report.html").read_text(encoding="utf-8")
|
|
header = page[page.index("<header"):page.index("</header>")]
|
|
for value in ("anthropic", "claude-opus-5-5", "openai", "gpt-6-luna", "market", "news",
|
|
"sec_edgar", "yfinance"):
|
|
assert value in _fields(header)
|
|
# Vendors are named by what they serve, and a chain keeps its order.
|
|
assert "Prices" in header and "Fundamentals" in header and "fundamental_data" not in header
|
|
assert header.index(">sec_edgar<") < header.rindex(">yfinance<")
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_contents_name_each_section_once_and_list_its_agents_beneath(tmp_path):
|
|
page = _html(tmp_path)
|
|
nav = page[page.index('<nav class="contents"'):page.index("</nav>")]
|
|
assert "<ol" not in nav # no list numbers beside the sections' own I-V
|
|
assert re.search(r'<a href="#s1"><span class="num">I</span>\s*<span class="label">Analyst Team Reports</span></a>', nav)
|
|
assert re.search(r'<a href="#s1-1"><span class="bar"></span>\s*<span class="label">Market Analyst</span></a>', nav)
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_section_rail_opens_on_hover_and_keyboard_focus(tmp_path):
|
|
page = _html(tmp_path)
|
|
css = page[page.index("<style>"):page.index("</style>")]
|
|
# Wide screens: a rail centred on the left edge whose names are hidden from
|
|
# sight only (still read by screen readers) until it is pointed at or focused.
|
|
assert "top: 50%" in css and "translateY(-50%)" in css
|
|
assert ".contents:hover .label" in css and ".contents:focus-within .label" in css
|
|
assert "display: none" not in css.split(".contents .label", 1)[1].split("}", 1)[0]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_sections_stay_within_reach(tmp_path):
|
|
page = _html(tmp_path)
|
|
nav = page[page.index('<nav class="contents"'):page.index("</nav>")]
|
|
assert "<h2" not in nav # the list needs no heading
|
|
assert 'id="top"' in page and 'href="#top"' in page # a way back on small screens
|
|
assert ".contents { position: fixed;" in page # beside the text on wide ones
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_page_heading_is_the_ticker_and_its_rating(tmp_path):
|
|
page = _html(tmp_path)
|
|
heading = re.search(r"<h1>(.*?)</h1>", page).group(1)
|
|
assert re.sub(r"<[^>]+>", "", heading).split() == ["NVDA", "Overweight"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_header_carries_the_official_logo_unaltered(tmp_path):
|
|
from importlib.resources import files
|
|
|
|
logo = files("tradingagents").joinpath("assets/tauric-logo.svg").read_text(encoding="utf-8").strip()
|
|
page = _html(tmp_path)
|
|
header = page[page.index("<header"):page.index("</header>")]
|
|
assert logo in header # drawn in the page, nothing fetched
|
|
assert 'href="https://tauric.ai"' in header and 'aria-label="Tauric Research"' in header
|
|
assert header.index('class="logo"') < header.index("<h1") # leads the header, above the title
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_page_credits_its_maker_in_a_line_of_text(tmp_path):
|
|
page = _html(tmp_path)
|
|
footer = page[page.index('<footer class="credit">'):page.index("</footer>")]
|
|
assert re.sub(r"<[^>]+>", "", footer).split() == ["TradingAgents", "powered", "by", "Tauric", "Research"]
|
|
# The logo already leads to tauric.ai; the credit leads to the project and the org.
|
|
assert 'href="https://github.com/TauricResearch/TradingAgents"' in footer
|
|
assert 'href="https://github.com/TauricResearch"' in footer and "tauric.ai" not in footer
|
|
assert "<svg" not in footer and 'class="brand"' in footer
|
|
assert page.index("</main>") < page.index('<footer class="credit">')
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_the_packaged_logo_stays_inert(tmp_path):
|
|
# The logo is drawn inside every report, so it may only ever hold shapes:
|
|
# no script, event handler, link, embedded document or external reference.
|
|
from importlib.resources import files
|
|
|
|
logo = files("tradingagents").joinpath("assets/tauric-logo.svg").read_text(encoding="utf-8")
|
|
tags = set(re.findall(r"<\s*([a-zA-Z][\w:-]*)", logo))
|
|
assert tags <= {"svg", "path", "rect", "circle", "g", "defs", "clipPath", "linearGradient", "stop"}
|
|
assert not re.search(r"\son\w+\s*=|href|url\(|@import|javascript:|<!entity|<!doctype", logo, re.IGNORECASE)
|