fix: repair DashboardState.__init__ (my prior commit orphaned _backtest_lock)

The self-heal commit (a00f967) inserted the _research method INSIDE __init__ at
method-body indent, truncating it so _change_signal/_backtest_lock/_backtest_
cache never got set -> AttributeError on backtest. Moved _research to a sibling
method; __init__ now sets all attrs. Two chart/sparkline tests updated: a None
research_con now REOPENS lazily, so 'no research db attached' is reached only
when the reopen also fails (they monkeypatch open to fail). 22 api tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-20 12:06:19 -05:00
1 parent a00f967bdf
commit e7bd8fa279
3 files changed
+17 -9

No files matched your search

+7 -7
View File
@@ -257,6 +257,13 @@ class DashboardState:
self.watch_inject = watch_inject self.watch_inject = watch_inject
self._lock = threading.Lock() self._lock = threading.Lock()
self._research_lock = threading.Lock() self._research_lock = threading.Lock()
# lazily-built, process-wide SSE change signal (shared by all clients)
self._change_signal: _ChangeSignal | None = None
# backtest replay cache (plan 015) — a DEDICATED lock, not self._lock,
# so a slow replay (10-30s) never blocks the live snapshot/SSE path.
# key = (since, until, repr(settings)) -> (result, monotonic timestamp).
self._backtest_lock = threading.Lock()
self._backtest_cache: dict = {}
def _research(self): def _research(self):
"""The research DB connection, opened LAZILY and self-healing. It used """The research DB connection, opened LAZILY and self-healing. It used
@@ -276,13 +283,6 @@ class DashboardState:
logger.warning("research DB open failed (charts empty): %s", exc) logger.warning("research DB open failed (charts empty): %s", exc)
self.research_con = None self.research_con = None
return self.research_con return self.research_con
# lazily-built, process-wide SSE change signal (shared by all clients)
self._change_signal: _ChangeSignal | None = None
# backtest replay cache (plan 015) — a DEDICATED lock, not self._lock,
# so a slow replay (10-30s) never blocks the live snapshot/SSE path.
# key = (since, until, repr(settings)) -> (result, monotonic timestamp).
self._backtest_lock = threading.Lock()
self._backtest_cache: dict = {}
def change_stamp(self): def change_stamp(self):
"""SSE change token from the shared once-per-second signal (see _ChangeSignal).""" """SSE change token from the shared once-per-second signal (see _ChangeSignal)."""
+5 -1
View File
@@ -93,7 +93,11 @@ def _get(state, path):
# Step 3: DashboardState.chart() — bars, markers, levels # Step 3: DashboardState.chart() — bars, markers, levels
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def test_chart_no_research_con_reports_missing_db(): def test_chart_no_research_con_reports_missing_db(monkeypatch):
# self-heal (2026-07-20): None con now REOPENS lazily; the "missing db"
# path is reached only when the reopen ALSO fails.
import api.main as m
monkeypatch.setattr(m, "open_research_db", lambda name: (_ for _ in ()).throw(RuntimeError("db down")))
out = DashboardState("momentum", research_con=None).chart("AAPL") out = DashboardState("momentum", research_con=None).chart("AAPL")
assert out["bars"] == [] assert out["bars"] == []
assert out.get("reason") == "no research db attached" assert out.get("reason") == "no research db attached"
+5 -1
View File
@@ -20,7 +20,11 @@ class _FakeResearchCon:
return _FakeResult() return _FakeResult()
def test_bars_without_reader_reports_missing_db(): def test_bars_without_reader_reports_missing_db(monkeypatch):
# self-heal (2026-07-20): None con reopens lazily; "missing db" only when
# the reopen also fails.
import api.main as m
monkeypatch.setattr(m, "open_research_db", lambda name: (_ for _ in ()).throw(RuntimeError("db down")))
out = DashboardState("momentum", research_con=None).bars("AAPL") out = DashboardState("momentum", research_con=None).bars("AAPL")
assert out["bars"] == [] assert out["bars"] == []
assert out.get("reason") == "no research db attached" assert out.get("reason") == "no research db attached"