From 21bc5d8fe7c878b4cd6053aee201bd317a39e86a Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Tue, 27 Oct 2026 00:52:43 +0000 Subject: [PATCH] fix(reporting): remove a previous run's sections when a report is saved to the same folder (#1566) - write_report_tree deletes the section files and HTML page this run did not write; other files in the folder are kept --- tests/test_reporting.py | 40 ++++++++++++++++++++++++++++++++++++++ tradingagents/reporting.py | 16 +++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/tests/test_reporting.py b/tests/test_reporting.py index b69858e1a..a9b9a8b02 100644 --- a/tests/test_reporting.py +++ b/tests/test_reporting.py @@ -49,6 +49,46 @@ def test_save_reports_explicit_path(tmp_path): assert out == tmp_path / "complete_report.md" +@pytest.mark.unit +@pytest.mark.parametrize("empty_value", [None, ""]) +def test_save_reports_replaces_previous_run_sections(tmp_path, empty_value): + graph = SimpleNamespace(run_settings=lambda: SETTINGS) + first = dict(_state(), trade_date="2026-09-22") + TradingAgentsGraph.save_reports(graph, first, "AAPL", save_path=tmp_path) + (tmp_path / "notes.md").write_text("User notes", encoding="utf-8") + (tmp_path / "1_analysts" / "notes.md").write_text("Analyst notes", encoding="utf-8") + + second = { + "trade_date": "2026-09-23", + "market_report": "NEW MARKET", + "news_report": empty_value, + "investment_debate_state": {"bull_history": empty_value}, + } + out = TradingAgentsGraph.save_reports(graph, second, "AAPL", save_path=tmp_path) + + assert (tmp_path / "1_analysts" / "market.md").read_text() == "NEW MARKET" + for filename in ( + "1_analysts/news.md", "2_research/bull.md", "2_research/manager.md", + "3_trading/trader.md", "4_risk/neutral.md", "5_portfolio/decision.md", + ): + assert not (tmp_path / filename).exists() + assert (tmp_path / "notes.md").read_text() == "User notes" + assert (tmp_path / "1_analysts" / "notes.md").read_text() == "Analyst notes" + assert "NEW MARKET" in out.read_text(encoding="utf-8") + assert "NEWS" not in out.read_text(encoding="utf-8") + assert "NEWS" not in (tmp_path / "complete_report.html").read_text(encoding="utf-8") + + +@pytest.mark.unit +def test_empty_report_removes_previous_sections_and_disabled_html(tmp_path): + write_report_tree(_state(), "AAPL", tmp_path) + out = write_report_tree({}, "AAPL", tmp_path, html=False) + + assert list(tmp_path.rglob("*.md")) == [out] + assert not (tmp_path / "complete_report.html").exists() + assert "Trading Analysis Report: AAPL" in out.read_text() + + @pytest.mark.unit def test_save_reports_defaults_under_results_dir(tmp_path): graph = object.__new__(TradingAgentsGraph) diff --git a/tradingagents/reporting.py b/tradingagents/reporting.py index 6f57e4a18..800f6a2a3 100644 --- a/tradingagents/reporting.py +++ b/tradingagents/reporting.py @@ -96,6 +96,8 @@ def write_report_tree(final_state: dict, ticker: str, save_path, settings: dict ``settings`` (``TradingAgentsGraph.run_settings()``) adds what produced the run to the report's header. ``html`` also writes ``complete_report.html``, one self-contained page of the same report (#1419). + Reusing a directory replaces the generated report files, removing sections + absent from this run and disabled HTML. Other files and directories are kept. """ save_path = Path(save_path) save_path.mkdir(parents=True, exist_ok=True) @@ -117,4 +119,18 @@ def write_report_tree(final_state: dict, ticker: str, save_path, settings: dict (save_path / "complete_report.html").write_text( render_report(ticker, final_state, settings, parts), encoding="utf-8" ) + else: + (save_path / "complete_report.html").unlink(missing_ok=True) + + # Reconcile only the filenames owned by this writer; never clear the tree, + # since callers may keep notes or other artifacts alongside the reports. + current_files = { + (folder, filename) + for _heading, folder, written in parts + for _agent, filename, _text in written + } + for _heading, folder, agents in _SECTIONS: + for _agent, filename, _path in agents: + if (folder, filename) not in current_files: + (save_path / folder / filename).unlink(missing_ok=True) return save_path / "complete_report.md"