From 4a71dc708dd116acc013955c13cc03a8bef417b4 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Wed, 23 Sep 2026 21:07:14 +0000 Subject: [PATCH] fix(cli): say whether a checkpointed run resumed or started fresh - the announcement existed but the run path never called it - tested through run_analysis rather than by calling the helper directly --- cli/main.py | 1 + tests/test_cli_commands.py | 17 --------------- tests/test_cli_decision_log.py | 39 ++++++++++++++++++++++++++++------ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/cli/main.py b/cli/main.py index 174740ad6..5b5d20952 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1174,6 +1174,7 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None): ) if checkpoint_tid is not None: args.setdefault("config", {}).setdefault("configurable", {})["thread_id"] = checkpoint_tid + _announce_checkpoint_state(graph, selections["ticker"], selections["analysis_date"]) # Stream the analysis. On resume, feed None so LangGraph continues the # interrupted run instead of re-appending the initial state (#1249); the diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index c0fb5f727..6ad20dac0 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -95,23 +95,6 @@ def test_every_command_is_registered_when_run_as_a_module(): assert "--start" in plain -@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.""" diff --git a/tests/test_cli_decision_log.py b/tests/test_cli_decision_log.py index 719fcba29..26e39c4cb 100644 --- a/tests/test_cli_decision_log.py +++ b/tests/test_cli_decision_log.py @@ -64,10 +64,12 @@ def test_record_decision_skips_a_run_without_a_decision(tmp_path): class _FakeGraph: """Records the lifecycle calls run_analysis makes.""" - def __init__(self): + def __init__(self, resuming=None): self.calls = [] self.graph = self self.propagator = self + self.resuming = resuming # None: checkpointing off + self._resuming = False def create_run_state(self, ticker, trade_date, asset_type="stock", portfolio=None): self.calls.append(("create_run_state", ticker, trade_date)) @@ -84,7 +86,8 @@ class _FakeGraph: return {} def begin_checkpoint(self, *a, **k): - return None + self._resuming = bool(self.resuming) + return None if self.resuming is None else "thread" def checkpoint_input(self, state): return state @@ -136,14 +139,14 @@ class _FakeBuffer: self.agent_status[agent] = status -@pytest.mark.unit -def test_cli_run_uses_the_decision_log_like_propagate(tmp_path, monkeypatch): +def _run_cli(monkeypatch, tmp_path, fake): + """Drive run_analysis against ``fake``; returns the message buffer.""" import cli.main as m from cli.models import AnalystType - fake = _FakeGraph() + buffer = _FakeBuffer() monkeypatch.setattr(m, "TradingAgentsGraph", lambda *a, **k: fake) - monkeypatch.setattr(m, "message_buffer", _FakeBuffer()) + monkeypatch.setattr(m, "message_buffer", buffer) monkeypatch.setattr(m, "create_layout", lambda: None) monkeypatch.setattr(m, "update_display", lambda *a, **k: None) monkeypatch.setattr(m, "Live", _NullLive) @@ -155,8 +158,14 @@ def test_cli_run_uses_the_decision_log_like_propagate(tmp_path, monkeypatch): "data_cache_dir": str(tmp_path / "cache"), "results_dir": str(tmp_path / "results"), }) monkeypatch.setattr(m.typer, "prompt", lambda *a, **k: "N") - m.run_analysis() + return buffer + + +@pytest.mark.unit +def test_cli_run_uses_the_decision_log_like_propagate(tmp_path, monkeypatch): + fake = _FakeGraph() + _run_cli(monkeypatch, tmp_path, fake) assert fake.calls == [ ("create_run_state", "NVDA", "2026-01-10"), @@ -165,3 +174,19 @@ def test_cli_run_uses_the_decision_log_like_propagate(tmp_path, monkeypatch): ("record_decision", "NVDA", "2026-01-10", "Rating: Buy\n\nBuy NVDA."), ("clear_checkpoint",), ] + + +@pytest.mark.unit +@pytest.mark.parametrize("resuming, said", [(True, "resuming"), (False, "starting fresh")]) +def test_the_cli_run_says_whether_it_resumed(tmp_path, monkeypatch, resuming, said): + """The README promises the run view tells a resumed run from a fresh one.""" + buffer = _run_cli(monkeypatch, tmp_path, _FakeGraph(resuming=resuming)) + + assert any(said in text.lower() for _, kind, text in buffer.messages if kind == "System") + + +@pytest.mark.unit +def test_a_run_without_checkpointing_says_nothing_about_resuming(tmp_path, monkeypatch): + buffer = _run_cli(monkeypatch, tmp_path, _FakeGraph()) + + assert not any("resum" in text.lower() or "fresh" in text.lower() for _, _, text in buffer.messages)