personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(home): surface backlog health in pulse

Add a shared read-only convey backlog loader and a stuck_day_rows renderer helper so home and health read the same backlog source without adding IO to backlog_view.

Require an explicit backlog signal for the home health glance and report stuck, unreadable, degraded, or stale backlog state before returning green.

+877 -68
+2 -13
solstone/apps/health/routes.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 - import json 5 4 import logging 6 - import os 7 5 import re 8 6 import socket 9 7 from datetime import datetime, timezone ··· 12 10 from flask import Blueprint, Response, current_app, jsonify, request 13 11 14 12 from solstone.convey import backlog_copy, state 13 + from solstone.convey.backlog_source import load_backlog_source 15 14 from solstone.convey.backlog_view import stuck_rows, verdict 16 15 from solstone.convey.reasons import ( 17 16 FILE_NOT_FOUND, ··· 59 58 60 59 61 60 def _load_backlog() -> dict | None: 62 - stats_path = os.path.join(state.journal_root, "stats.json") 63 - if not os.path.isfile(stats_path): 64 - return None 65 - try: 66 - with open(stats_path, "r", encoding="utf-8") as f: 67 - data = json.load(f) 68 - except (OSError, ValueError): 69 - logger.exception("Failed to read backlog from stats.json") 70 - return None 71 - backlog = data.get("backlog") 72 - return backlog if isinstance(backlog, dict) else None 61 + return load_backlog_source(state.journal_root).backlog 73 62 74 63 75 64 def _safe_brain_snapshot() -> dict:
+127
solstone/apps/home/health_glance.py
··· 4 4 from __future__ import annotations 5 5 6 6 import logging 7 + from datetime import datetime, timezone 7 8 from typing import Any 8 9 9 10 from solstone.apps.home.needs_you import format_degraded_capture_line 11 + from solstone.convey.backlog_source import BacklogSource 12 + from solstone.convey.backlog_view import stuck_day_rows 13 + from solstone.convey.provider_readiness import DISPLAY_NAMES 10 14 11 15 logger = logging.getLogger(__name__) 12 16 13 17 _HEALTH_DETAIL_HREF = "/app/health#focus=recent-errors&day=today" 18 + BACKLOG_FRESHNESS_MAX_AGE_HOURS = 36 19 + _BACKLOG_HREF = "/app/health" 14 20 15 21 16 22 def build_health_glance( 17 23 capture_health: Any, 18 24 pipeline_status: Any, 19 25 last_observe_relative: str | None, 26 + *, 27 + backlog: BacklogSource, 20 28 brain: dict[str, Any] | None = None, 21 29 ) -> dict: 22 30 issues = [] 31 + 32 + issues.extend(_build_backlog_issues(backlog)) 23 33 24 34 capture_issue = _issue_safely( 25 35 "capture health", capture_health, _build_capture_issue ··· 104 114 except Exception: 105 115 logger.warning("omitting malformed %s signal", label, exc_info=True) 106 116 return None 117 + 118 + 119 + def _build_backlog_issues(source: BacklogSource) -> list[dict]: 120 + issues: list[dict] = [] 121 + if source.validity != "valid": 122 + issues.append(_backlog_unknown_issue()) 123 + return issues 124 + 125 + backlog = source.backlog 126 + if not isinstance(backlog, dict): 127 + issues.append(_backlog_unknown_issue()) 128 + return issues 129 + 130 + if backlog.get("degraded") is True: 131 + issues.append(_backlog_unknown_issue()) 132 + 133 + freshness_issue = _build_backlog_freshness_issue(source.generated_at) 134 + if freshness_issue is not None: 135 + issues.append(freshness_issue) 136 + 137 + if _backlog_count(backlog.get("stuck_days")) > 0: 138 + issues.append(_build_backlog_stuck_issue(backlog)) 139 + 140 + return issues 141 + 142 + 143 + def _backlog_unknown_issue() -> dict: 144 + return { 145 + "text": "i can't tell if your journal is caught up right now.", 146 + "severity": "amber", 147 + "href": _BACKLOG_HREF, 148 + } 149 + 150 + 151 + def _build_backlog_freshness_issue(generated_at: str | None) -> dict | None: 152 + generated_at_dt = _parse_generated_at(generated_at) 153 + if generated_at_dt is None: 154 + return { 155 + "text": ( 156 + "i can't tell if your journal is caught up; " 157 + "the last update age is unknown." 158 + ), 159 + "severity": "amber", 160 + "href": _BACKLOG_HREF, 161 + } 162 + 163 + age = _now_utc() - generated_at_dt 164 + if age.total_seconds() <= BACKLOG_FRESHNESS_MAX_AGE_HOURS * 3600: 165 + return None 166 + return { 167 + "text": ( 168 + "i can't tell if your journal is caught up; " 169 + f"the last update was {_format_age(age)} ago." 170 + ), 171 + "severity": "amber", 172 + "href": _BACKLOG_HREF, 173 + } 174 + 175 + 176 + def _build_backlog_stuck_issue(backlog: dict) -> dict: 177 + rows = stuck_day_rows(backlog) 178 + if rows: 179 + text = _backlog_stuck_issue_text(rows[0]) 180 + else: 181 + text = "a journal day needs a hand." 182 + return {"text": text, "severity": "red", "href": _BACKLOG_HREF} 183 + 184 + 185 + def _backlog_stuck_issue_text(row: dict) -> str: 186 + reason = row.get("reason") 187 + text = reason.strip() if isinstance(reason, str) and reason.strip() else "" 188 + if not text: 189 + text = "a journal day needs a hand." 190 + 191 + provider = row.get("provider") 192 + display_name = DISPLAY_NAMES.get(provider) if isinstance(provider, str) else None 193 + if display_name: 194 + return f"{display_name}: {text}" 195 + return text 196 + 197 + 198 + def _backlog_count(value: Any) -> int: 199 + try: 200 + count = int(value) 201 + except (TypeError, ValueError): 202 + return 0 203 + return max(0, count) 204 + 205 + 206 + def _parse_generated_at(value: str | None) -> datetime | None: 207 + if not isinstance(value, str) or not value.strip(): 208 + return None 209 + text = value.strip() 210 + if text.endswith("Z"): 211 + text = f"{text[:-1]}+00:00" 212 + try: 213 + parsed = datetime.fromisoformat(text) 214 + except ValueError: 215 + return None 216 + if parsed.tzinfo is None: 217 + parsed = parsed.replace(tzinfo=timezone.utc) 218 + return parsed.astimezone(timezone.utc) 219 + 220 + 221 + def _format_age(delta: Any) -> str: 222 + seconds = max(0, int(delta.total_seconds())) 223 + hours = seconds // 3600 224 + if hours >= 1: 225 + unit = "hour" if hours == 1 else "hours" 226 + return f"{hours} {unit}" 227 + minutes = max(1, seconds // 60) 228 + unit = "minute" if minutes == 1 else "minutes" 229 + return f"{minutes} {unit}" 230 + 231 + 232 + def _now_utc() -> datetime: 233 + return datetime.now(timezone.utc) 107 234 108 235 109 236 def _build_capture_issue(capture_health: Any) -> dict | None:
+4
solstone/apps/home/routes.py
··· 20 20 from solstone.apps.home.health_glance import build_health_glance 21 21 from solstone.apps.home.needs_you import classify_needs_you, needs_dedup_key 22 22 from solstone.apps.home.owner_voice import build_owner_voice_needs 23 + from solstone.convey import state 24 + from solstone.convey.backlog_source import load_backlog_source 23 25 from solstone.convey.bridge import get_cached_state 24 26 from solstone.convey.shell_data import _resolve_attention 25 27 from solstone.convey.utils import DATE_RE, format_date, relative_time ··· 921 923 if summary: 922 924 pipeline_status = {**pipeline_status, **summary} 923 925 brain = build_brain_snapshot(datetime.now(timezone.utc), surface="home") 926 + backlog_source = load_backlog_source(state.journal_root) 924 927 health_glance = build_health_glance( 925 928 capture_health, 926 929 pipeline_status, 927 930 last_observe_relative, 931 + backlog=backlog_source, 928 932 brain=brain, 929 933 ) 930 934
+48 -9
solstone/apps/home/tests/test_health_glance_brain.py
··· 21 21 finish_brain_refresh, 22 22 ) 23 23 from solstone.think.providers.runtime_health import runtime_health_path 24 + from tests.helpers.health_glance import healthy_backlog_source 24 25 25 26 NOW = datetime(2026, 7, 21, 12, 0, 0, tzinfo=timezone.utc) 26 27 BUNDLED_RUNTIME_FINGERPRINT = "b" * 64 ··· 174 175 assert permit is not None 175 176 try: 176 177 brain = build_brain_snapshot(NOW, surface="home", journal_path=journal) 177 - result = build_health_glance(_active_capture(), None, "5m ago", brain=brain) 178 + result = build_health_glance( 179 + _active_capture(), 180 + None, 181 + "5m ago", 182 + brain=brain, 183 + backlog=healthy_backlog_source(), 184 + ) 178 185 179 186 assert brain["state"] == "checking" 180 187 assert brain["headline"] == HEADLINES["checking"] ··· 196 203 fresh_now, surface="home", journal_path=journal 197 204 ) 198 205 fresh_result = build_health_glance( 199 - _active_capture(), None, "5m ago", brain=fresh_brain 206 + _active_capture(), 207 + None, 208 + "5m ago", 209 + brain=fresh_brain, 210 + backlog=healthy_backlog_source(), 200 211 ) 201 212 assert fresh_brain["state"] == "checking" 202 213 _assert_checking_row(fresh_result) ··· 209 220 expired_now, surface="home", journal_path=journal 210 221 ) 211 222 expired_result = build_health_glance( 212 - _active_capture(), None, "5m ago", brain=expired_brain 223 + _active_capture(), 224 + None, 225 + "5m ago", 226 + brain=expired_brain, 227 + backlog=healthy_backlog_source(), 213 228 ) 214 229 215 230 assert expired_brain["state"] == "unknown" ··· 232 247 permit.release() 233 248 234 249 brain = build_brain_snapshot(NOW, surface="home", journal_path=journal) 235 - result = build_health_glance(_active_capture(), None, "5m ago", brain=brain) 250 + result = build_health_glance( 251 + _active_capture(), None, "5m ago", brain=brain, backlog=healthy_backlog_source() 252 + ) 236 253 237 254 assert brain["state"] == "unknown" 238 255 assert brain["reason_code"] == "brain_check_interrupted" ··· 254 271 _write_runtime_health_record(journal, phase="starting") 255 272 progressing_brain = build_brain_snapshot(NOW, surface="home", journal_path=journal) 256 273 progressing_result = build_health_glance( 257 - _active_capture(), None, "5m ago", brain=progressing_brain 274 + _active_capture(), 275 + None, 276 + "5m ago", 277 + brain=progressing_brain, 278 + backlog=healthy_backlog_source(), 258 279 ) 259 280 260 281 assert progressing_brain["state"] == "blocked" ··· 266 287 _write_runtime_health_record(journal, phase="backoff") 267 288 blocked_brain = build_brain_snapshot(NOW, surface="home", journal_path=journal) 268 289 blocked_result = build_health_glance( 269 - _active_capture(), None, "5m ago", brain=blocked_brain 290 + _active_capture(), 291 + None, 292 + "5m ago", 293 + brain=blocked_brain, 294 + backlog=healthy_backlog_source(), 270 295 ) 271 296 272 297 assert blocked_brain["state"] == "blocked" ··· 287 312 try: 288 313 brain = build_brain_snapshot(NOW, surface="home", journal_path=journal) 289 314 290 - active = build_health_glance(_active_capture(), None, "5m ago", brain=brain) 315 + active = build_health_glance( 316 + _active_capture(), 317 + None, 318 + "5m ago", 319 + brain=brain, 320 + backlog=healthy_backlog_source(), 321 + ) 291 322 no_observers = build_health_glance( 292 - _no_observers_capture(), None, None, brain=brain 323 + _no_observers_capture(), 324 + None, 325 + None, 326 + brain=brain, 327 + backlog=healthy_backlog_source(), 293 328 ) 294 329 unavailable = build_health_glance( 295 - _unavailable_capture(), None, None, brain=brain 330 + _unavailable_capture(), 331 + None, 332 + None, 333 + brain=brain, 334 + backlog=healthy_backlog_source(), 296 335 ) 297 336 298 337 assert brain["state"] == "checking"
+53
solstone/convey/backlog_source.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Read-only loader for the serialized journal backlog source.""" 5 + 6 + from __future__ import annotations 7 + 8 + import json 9 + from dataclasses import dataclass 10 + from pathlib import Path 11 + from typing import Any 12 + 13 + 14 + @dataclass(frozen=True) 15 + class BacklogSource: 16 + backlog: dict | None 17 + validity: str 18 + generated_at: str | None 19 + 20 + 21 + def load_backlog_source(journal_root: str) -> BacklogSource: 22 + path = Path(journal_root) / "stats.json" 23 + try: 24 + with path.open("r", encoding="utf-8") as handle: 25 + data: Any = json.load(handle) 26 + except FileNotFoundError: 27 + return BacklogSource(backlog=None, validity="missing", generated_at=None) 28 + except (OSError, ValueError): 29 + return BacklogSource(backlog=None, validity="unparseable", generated_at=None) 30 + 31 + if not isinstance(data, dict): 32 + return BacklogSource(backlog=None, validity="malformed", generated_at=None) 33 + 34 + generated_at = data.get("generated_at") 35 + if not isinstance(generated_at, str): 36 + generated_at = None 37 + 38 + if "backlog" not in data: 39 + return BacklogSource( 40 + backlog=None, 41 + validity="no_backlog_key", 42 + generated_at=generated_at, 43 + ) 44 + 45 + backlog = data.get("backlog") 46 + if not isinstance(backlog, dict): 47 + return BacklogSource( 48 + backlog=None, 49 + validity="malformed", 50 + generated_at=generated_at, 51 + ) 52 + 53 + return BacklogSource(backlog=backlog, validity="valid", generated_at=generated_at)
+27 -10
solstone/convey/backlog_view.py
··· 11 11 from solstone.convey import backlog_copy, provider_readiness 12 12 13 13 __all__ = [ 14 + "stuck_day_rows", 14 15 "stuck_rows", 15 16 "verdict", 16 17 ] ··· 110 111 return backlog_copy.BACKLOG_REASON_FAILING_STEP 111 112 112 113 114 + def _stuck_row(day: dict) -> dict: 115 + depth = _count(day.get("segments")) + _count(day.get("units")) 116 + row = { 117 + "day": day.get("day"), 118 + "reason": _reason_copy(day), 119 + "depth": depth if depth > 0 else None, 120 + } 121 + for field in ("reason_code", "provider", "model"): 122 + if day.get(field): 123 + row[field] = day.get(field) 124 + return row 125 + 126 + 113 127 def stuck_rows(backlog: dict | None) -> list[dict]: 114 128 if backlog is None or backlog.get("degraded") is True: 115 129 return [] ··· 118 132 for day in backlog.get("days") or []: 119 133 if not _needs_hand(day, backlog): 120 134 continue 121 - depth = _count(day.get("segments")) + _count(day.get("units")) 122 - row = { 123 - "day": day.get("day"), 124 - "reason": _reason_copy(day), 125 - "depth": depth if depth > 0 else None, 126 - } 127 - for field in ("reason_code", "provider", "model"): 128 - if day.get(field): 129 - row[field] = day.get(field) 130 - rows.append(row) 135 + rows.append(_stuck_row(day)) 136 + return rows 137 + 138 + 139 + def stuck_day_rows(backlog: dict | None) -> list[dict]: 140 + if backlog is None or backlog.get("degraded") is True: 141 + return [] 142 + 143 + rows = [] 144 + for day in backlog.get("days") or []: 145 + if day.get("state") != "stuck": 146 + continue 147 + rows.append(_stuck_row(day)) 131 148 return rows
+22
tests/helpers/health_glance.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + from datetime import datetime, timezone 7 + 8 + from solstone.convey.backlog_source import BacklogSource 9 + 10 + 11 + def healthy_backlog_source() -> BacklogSource: 12 + return BacklogSource( 13 + backlog={ 14 + "pending_days": 0, 15 + "stuck_days": 0, 16 + "days": [], 17 + "errors": [], 18 + "degraded": False, 19 + }, 20 + validity="valid", 21 + generated_at=datetime.now(timezone.utc).isoformat(), 22 + )
+49 -1
tests/test_backlog_view.py
··· 6 6 import pytest 7 7 8 8 from solstone.convey import backlog_copy 9 - from solstone.convey.backlog_view import stuck_rows, verdict 9 + from solstone.convey.backlog_view import stuck_day_rows, stuck_rows, verdict 10 10 11 11 12 12 def _assert_single_stuck_reason(reason_code: str, expected: str) -> None: ··· 166 166 "reason": backlog_copy.BACKLOG_REASON_CORRUPT_RAW, 167 167 "depth": 1, 168 168 }, 169 + ] 170 + 171 + 172 + def test_stuck_day_rows_excludes_pending_error_rows_without_changing_row_shape(): 173 + backlog = { 174 + "days": [ 175 + { 176 + "day": "20260601", 177 + "state": "pending", 178 + "error": {"reason_code": "provider_request_rejected"}, 179 + "reason_code": "provider_request_rejected", 180 + "provider": "google", 181 + }, 182 + { 183 + "day": "20260602", 184 + "state": "stuck", 185 + "segments": 1, 186 + "reason_code": "provider_request_rejected", 187 + "provider": "google", 188 + }, 189 + ], 190 + "errors": [], 191 + } 192 + 193 + assert stuck_rows(backlog) == [ 194 + { 195 + "day": "20260601", 196 + "reason": backlog_copy.BACKLOG_REASON_PROVIDER_REFUSED, 197 + "depth": None, 198 + "reason_code": "provider_request_rejected", 199 + "provider": "google", 200 + }, 201 + { 202 + "day": "20260602", 203 + "reason": backlog_copy.BACKLOG_REASON_PROVIDER_REFUSED, 204 + "depth": 1, 205 + "reason_code": "provider_request_rejected", 206 + "provider": "google", 207 + }, 208 + ] 209 + assert stuck_day_rows(backlog) == [ 210 + { 211 + "day": "20260602", 212 + "reason": backlog_copy.BACKLOG_REASON_PROVIDER_REFUSED, 213 + "depth": 1, 214 + "reason_code": "provider_request_rejected", 215 + "provider": "google", 216 + } 169 217 ] 170 218 171 219
+2
tests/test_brain_health_cutover_parity.py
··· 9 9 from flask import Flask 10 10 11 11 from solstone.think.brain_health import HEADLINES 12 + from tests.helpers.health_glance import healthy_backlog_source 12 13 13 14 14 15 @dataclass(frozen=True) ··· 158 159 None, 159 160 "5m ago", 160 161 brain=home_brain, 162 + backlog=healthy_backlog_source(), 161 163 ) 162 164 home_issue = home["issues"][0] if home["issues"] else None 163 165 assert home_brain["headline"] == HEADLINES[case.state]
+6
tests/test_home_connections.py
··· 16 16 from solstone.think.indexer import edges as edge_index 17 17 from solstone.think.indexer.edges import insert_edges 18 18 from solstone.think.indexer.journal import get_journal_index 19 + from tests.helpers.health_glance import healthy_backlog_source 19 20 20 21 EDGE_FIXTURE = Path(__file__).resolve().parent / "fixtures" / "edges_journal" 21 22 CONTRACT_KIND_WORDS = { ··· 264 265 monkeypatch.setattr(home_routes, "read_steward_health", lambda: None) 265 266 monkeypatch.setattr( 266 267 home_routes, "read_steward_summary", lambda *args, **kwargs: None 268 + ) 269 + monkeypatch.setattr( 270 + home_routes, 271 + "load_backlog_source", 272 + lambda _journal_root: healthy_backlog_source(), 267 273 ) 268 274 monkeypatch.setattr( 269 275 home_routes,
+479 -34
tests/test_home_health_glance.py
··· 3 3 4 4 from __future__ import annotations 5 5 6 + import json 6 7 import re 7 - from datetime import datetime 8 + from datetime import datetime, timedelta, timezone 8 9 9 10 import pytest 10 11 12 + from solstone.apps.home import health_glance as health_glance_module 11 13 from solstone.apps.home.health_glance import build_health_glance 14 + from solstone.convey.backlog_source import BacklogSource, load_backlog_source 12 15 from solstone.think.brain_health import HEADLINES 16 + from tests.helpers.health_glance import healthy_backlog_source 13 17 14 18 BANNED_RE = re.compile( 15 19 r"\b(watch|capture|record|monitor|track|collect|observer|observation)\b", ··· 23 27 re.IGNORECASE, 24 28 ) 25 29 HEALTH_DETAIL_HREF = "/app/health#focus=recent-errors&day=today" 30 + BACKLOG_NOW = datetime(2026, 7, 21, 12, 0, 0, tzinfo=timezone.utc) 26 31 27 32 28 33 def _brain( ··· 114 119 return {"status": "no_observers", "observers": []} 115 120 116 121 122 + def _backlog_source( 123 + backlog: dict | None, 124 + *, 125 + validity: str = "valid", 126 + generated_at: str | None = None, 127 + ) -> BacklogSource: 128 + return BacklogSource( 129 + backlog=backlog, 130 + validity=validity, 131 + generated_at=generated_at 132 + if generated_at is not None 133 + else BACKLOG_NOW.isoformat(), 134 + ) 135 + 136 + 137 + def _clear_backlog() -> dict: 138 + return { 139 + "pending_days": 0, 140 + "stuck_days": 0, 141 + "days": [], 142 + "errors": [], 143 + "degraded": False, 144 + } 145 + 146 + 147 + def _stuck_backlog(day: dict | None = None) -> dict: 148 + return { 149 + "pending_days": 0, 150 + "stuck_days": 1, 151 + "days": [ 152 + day 153 + or { 154 + "day": "20260720", 155 + "state": "stuck", 156 + "reason_code": "provider_request_rejected", 157 + "provider": "google", 158 + "segments": 1, 159 + } 160 + ], 161 + "errors": [], 162 + "degraded": False, 163 + } 164 + 165 + 166 + @pytest.fixture 167 + def fixed_backlog_now(monkeypatch: pytest.MonkeyPatch) -> None: 168 + monkeypatch.setattr(health_glance_module, "_now_utc", lambda: BACKLOG_NOW) 169 + 170 + 117 171 def _assert_status_row(result: dict, *, verdict: str, headline: str) -> None: 118 172 assert result["verdict"] == verdict 119 173 assert result["severity"] == "amber" ··· 154 208 assert result["issues"][0] == {"text": text, "severity": "amber", "href": href} 155 209 156 210 211 + def test_stuck_backlog_returns_red_attention_with_display_provider( 212 + fixed_backlog_now: None, 213 + ) -> None: 214 + result = build_health_glance( 215 + _active_capture(), 216 + None, 217 + "5m ago", 218 + backlog=_backlog_source(_stuck_backlog()), 219 + ) 220 + 221 + assert result["verdict"] == "attention" 222 + assert result["severity"] == "red" 223 + assert any( 224 + "the AI provider refused a request sol sent" in issue["text"] 225 + and "Gemini" in issue["text"] 226 + and "google" not in issue["text"] 227 + for issue in result["issues"] 228 + ) 229 + 230 + 231 + def test_pending_only_backlog_with_error_row_does_not_raise_backlog_issue( 232 + fixed_backlog_now: None, 233 + ) -> None: 234 + backlog = { 235 + "pending_days": 1, 236 + "stuck_days": 0, 237 + "days": [ 238 + { 239 + "day": "20260720", 240 + "state": "pending", 241 + "error": {"reason_code": "provider_request_rejected"}, 242 + "reason_code": "provider_request_rejected", 243 + "provider": "google", 244 + } 245 + ], 246 + "errors": [], 247 + "degraded": False, 248 + } 249 + 250 + result = build_health_glance( 251 + _active_capture(), None, "5m ago", backlog=_backlog_source(backlog) 252 + ) 253 + 254 + assert result["verdict"] == "ok" 255 + assert result["severity"] == "green" 256 + assert result["issues"] == [] 257 + 258 + 259 + def test_zero_backlog_active_observer_uses_existing_everything_working_green_return( 260 + fixed_backlog_now: None, 261 + ) -> None: 262 + result = build_health_glance( 263 + _active_capture(), None, "5m ago", backlog=_backlog_source(_clear_backlog()) 264 + ) 265 + 266 + assert result["verdict"] == "ok" 267 + assert result["severity"] == "green" 268 + assert result["headline"] == "everything's working" 269 + assert result["last_observation"] == "5m ago" 270 + assert result["issues"] == [] 271 + 272 + 273 + def test_stuck_backlog_with_absent_pipeline_status_still_returns_attention( 274 + fixed_backlog_now: None, 275 + ) -> None: 276 + result = build_health_glance( 277 + _active_capture(), None, None, backlog=_backlog_source(_stuck_backlog()) 278 + ) 279 + 280 + assert result["verdict"] == "attention" 281 + assert result["severity"] == "red" 282 + 283 + 284 + @pytest.mark.parametrize( 285 + "source", 286 + [ 287 + BacklogSource(backlog=None, validity="missing", generated_at=None), 288 + BacklogSource(backlog=None, validity="unparseable", generated_at=None), 289 + BacklogSource( 290 + backlog=None, 291 + validity="no_backlog_key", 292 + generated_at=BACKLOG_NOW.isoformat(), 293 + ), 294 + BacklogSource( 295 + backlog=None, 296 + validity="malformed", 297 + generated_at=BACKLOG_NOW.isoformat(), 298 + ), 299 + _backlog_source({**_clear_backlog(), "degraded": True}), 300 + ], 301 + ) 302 + def test_unreadable_or_degraded_backlog_source_is_not_green( 303 + source: BacklogSource, 304 + fixed_backlog_now: None, 305 + ) -> None: 306 + result = build_health_glance(_active_capture(), None, "5m ago", backlog=source) 307 + 308 + assert result["verdict"] == "attention" 309 + assert result["severity"] == "amber" 310 + assert any("i can't tell" in issue["text"] for issue in result["issues"]) 311 + 312 + 313 + def test_backlog_loader_outcomes_feed_non_green_glance( 314 + tmp_path, 315 + fixed_backlog_now: None, 316 + ) -> None: 317 + cases = [ 318 + ("missing", None, "missing"), 319 + ("unparseable", "{not-json", "unparseable"), 320 + ( 321 + "no_backlog_key", 322 + {"generated_at": BACKLOG_NOW.isoformat(), "schema_version": 1}, 323 + "no_backlog_key", 324 + ), 325 + ( 326 + "malformed", 327 + {"generated_at": BACKLOG_NOW.isoformat(), "backlog": []}, 328 + "malformed", 329 + ), 330 + ( 331 + "degraded", 332 + { 333 + "generated_at": BACKLOG_NOW.isoformat(), 334 + "backlog": {**_clear_backlog(), "degraded": True}, 335 + }, 336 + "valid", 337 + ), 338 + ] 339 + 340 + for name, payload, expected_validity in cases: 341 + journal = tmp_path / name 342 + journal.mkdir() 343 + if isinstance(payload, str): 344 + (journal / "stats.json").write_text(payload, encoding="utf-8") 345 + elif payload is not None: 346 + (journal / "stats.json").write_text(json.dumps(payload), encoding="utf-8") 347 + 348 + source = load_backlog_source(str(journal)) 349 + result = build_health_glance(_active_capture(), None, "5m ago", backlog=source) 350 + 351 + assert source.validity == expected_validity 352 + assert result["verdict"] == "attention" 353 + assert result["severity"] == "amber" 354 + assert any("i can't tell" in issue["text"] for issue in result["issues"]) 355 + 356 + 357 + def test_backlog_issue_builder_exceptions_are_not_safely_omitted( 358 + monkeypatch: pytest.MonkeyPatch, 359 + fixed_backlog_now: None, 360 + ) -> None: 361 + def raise_from_backlog(_backlog: dict) -> list[dict]: 362 + raise RuntimeError("backlog boom") 363 + 364 + monkeypatch.setattr(health_glance_module, "stuck_day_rows", raise_from_backlog) 365 + 366 + with pytest.raises(RuntimeError, match="backlog boom"): 367 + build_health_glance( 368 + _active_capture(), None, "5m ago", backlog=_backlog_source(_stuck_backlog()) 369 + ) 370 + 371 + 372 + def test_backlog_freshness_controls_green_vs_stale_issue( 373 + fixed_backlog_now: None, 374 + ) -> None: 375 + fresh = build_health_glance( 376 + _active_capture(), 377 + None, 378 + "5m ago", 379 + backlog=_backlog_source( 380 + _clear_backlog(), 381 + generated_at=(BACKLOG_NOW - timedelta(hours=35)).isoformat(), 382 + ), 383 + ) 384 + stale = build_health_glance( 385 + _active_capture(), 386 + None, 387 + "5m ago", 388 + backlog=_backlog_source( 389 + _clear_backlog(), 390 + generated_at=(BACKLOG_NOW - timedelta(hours=37)).isoformat(), 391 + ), 392 + ) 393 + 394 + assert fresh["verdict"] == "ok" 395 + assert stale["verdict"] == "attention" 396 + assert stale["severity"] == "amber" 397 + assert any( 398 + "i can't tell" in issue["text"] and "37 hours ago" in issue["text"] 399 + for issue in stale["issues"] 400 + ) 401 + 402 + 403 + @pytest.mark.parametrize("generated_at", [None, "not-a-timestamp"]) 404 + def test_backlog_missing_or_unparseable_generated_at_is_not_fresh( 405 + generated_at: str | None, 406 + fixed_backlog_now: None, 407 + ) -> None: 408 + result = build_health_glance( 409 + _active_capture(), 410 + None, 411 + "5m ago", 412 + backlog=BacklogSource( 413 + backlog=_clear_backlog(), 414 + validity="valid", 415 + generated_at=generated_at, 416 + ), 417 + ) 418 + 419 + assert result["verdict"] == "attention" 420 + assert result["severity"] == "amber" 421 + assert any( 422 + "i can't tell" in issue["text"] and "age is unknown" in issue["text"] 423 + for issue in result["issues"] 424 + ) 425 + 426 + 427 + @pytest.mark.parametrize( 428 + "day", 429 + [ 430 + { 431 + "day": "20260720", 432 + "state": "stuck", 433 + "reason_code": "provider_request_rejected", 434 + }, 435 + { 436 + "day": "20260720", 437 + "state": "stuck", 438 + "reason_code": "provider_request_rejected", 439 + "provider": "weirdslug", 440 + }, 441 + ], 442 + ) 443 + def test_stuck_backlog_without_display_provider_degrades_cleanly( 444 + day: dict, 445 + fixed_backlog_now: None, 446 + ) -> None: 447 + result = build_health_glance( 448 + _active_capture(), None, "5m ago", backlog=_backlog_source(_stuck_backlog(day)) 449 + ) 450 + 451 + assert result["verdict"] == "attention" 452 + assert result["severity"] == "red" 453 + text = result["issues"][0]["text"] 454 + assert "None" not in text 455 + assert "weirdslug" not in text 456 + 457 + 458 + def test_backlog_argument_is_required_and_keyword_only() -> None: 459 + with pytest.raises(TypeError): 460 + build_health_glance(_active_capture(), None, "5m ago") 461 + 462 + with pytest.raises(TypeError): 463 + build_health_glance( 464 + _active_capture(), 465 + None, 466 + "5m ago", 467 + healthy_backlog_source(), 468 + ) 469 + 470 + 157 471 def test_degraded_capture_returns_red_attention_issue(): 158 - result = build_health_glance(_degraded_capture("fedora"), None, None) 472 + result = build_health_glance( 473 + _degraded_capture("fedora"), None, None, backlog=healthy_backlog_source() 474 + ) 159 475 160 476 assert result["verdict"] == "attention" 161 477 assert result["severity"] == "red" ··· 171 487 172 488 def test_degraded_capture_collapses_multiple_observers_to_one_issue(): 173 489 result = build_health_glance( 174 - _degraded_capture("fedora", "phone", "tablet"), None, None 490 + _degraded_capture("fedora", "phone", "tablet"), 491 + None, 492 + None, 493 + backlog=healthy_backlog_source(), 175 494 ) 176 495 177 496 assert len(result["issues"]) == 1 ··· 186 505 _degraded_capture("fedora"), 187 506 {"status": "warning", "headline": "processing needs attention"}, 188 507 None, 508 + backlog=healthy_backlog_source(), 189 509 ) 190 510 191 511 assert result["verdict"] == "attention" ··· 199 519 _active_capture(), 200 520 {"status": "warning", "headline": "processing needs attention"}, 201 521 None, 522 + backlog=healthy_backlog_source(), 202 523 ) 203 524 204 525 assert result["verdict"] == "attention" ··· 213 534 _active_capture(), 214 535 {"status": "warning", "message": "some bullet"}, 215 536 None, 537 + backlog=healthy_backlog_source(), 216 538 ) 217 539 218 540 assert result["verdict"] != "ok" ··· 234 556 ], 235 557 ) 236 558 def test_offline_and_stale_capture_do_not_return_ok(capture_health, issue_text): 237 - result = build_health_glance(capture_health, None, None) 559 + result = build_health_glance( 560 + capture_health, None, None, backlog=healthy_backlog_source() 561 + ) 238 562 239 563 assert result["verdict"] != "ok" 240 564 assert result["headline"] != "everything's working" ··· 242 566 243 567 244 568 def test_active_capture_without_pipeline_returns_ok_with_last_observation(): 245 - result = build_health_glance(_active_capture(), None, "5m ago") 569 + result = build_health_glance( 570 + _active_capture(), None, "5m ago", backlog=healthy_backlog_source() 571 + ) 246 572 247 573 assert result["verdict"] == "ok" 248 574 assert result["severity"] == "green" ··· 253 579 254 580 def test_no_observers_returns_ok_with_setup_cta(): 255 581 result = build_health_glance( 256 - {"status": "no_observers", "observers": []}, None, None 582 + {"status": "no_observers", "observers": []}, 583 + None, 584 + None, 585 + backlog=healthy_backlog_source(), 257 586 ) 258 587 259 588 assert result["verdict"] == "ok" ··· 267 596 268 597 269 598 def test_unknown_observer_state_returns_unavailable(): 270 - result = build_health_glance({"status": "unknown", "observers": []}, None, None) 599 + result = build_health_glance( 600 + {"status": "unknown", "observers": []}, 601 + None, 602 + None, 603 + backlog=healthy_backlog_source(), 604 + ) 271 605 272 606 assert result["verdict"] == "unavailable" 273 607 assert result["severity"] == "amber" ··· 285 619 "suggested_action": "open_support", 286 620 }, 287 621 None, 622 + backlog=healthy_backlog_source(), 288 623 ) 289 624 290 625 assert result["issues"][0]["href"] == "/app/support" ··· 307 642 ], 308 643 ) 309 644 def test_pipeline_health_actions_route_to_recent_errors(pipeline_status): 310 - result = build_health_glance(_active_capture(), pipeline_status, None) 645 + result = build_health_glance( 646 + _active_capture(), pipeline_status, None, backlog=healthy_backlog_source() 647 + ) 311 648 312 649 assert result["issues"][0]["href"] == HEALTH_DETAIL_HREF 313 650 314 651 315 652 def test_brain_blocked_alone_returns_amber_attention_issue(): 316 - result = build_health_glance(_active_capture(), None, None, brain=_blocked_brain()) 653 + result = build_health_glance( 654 + _active_capture(), 655 + None, 656 + None, 657 + brain=_blocked_brain(), 658 + backlog=healthy_backlog_source(), 659 + ) 317 660 318 661 assert result["issues"] == [ 319 662 { ··· 329 672 330 673 def test_brain_blocked_combines_with_red_capture_issue(): 331 674 result = build_health_glance( 332 - _degraded_capture("fedora"), None, None, brain=_blocked_brain() 675 + _degraded_capture("fedora"), 676 + None, 677 + None, 678 + brain=_blocked_brain(), 679 + backlog=healthy_backlog_source(), 333 680 ) 334 681 335 682 assert len(result["issues"]) == 2 ··· 346 693 347 694 def test_brain_ready_keeps_ok_glance(): 348 695 result = build_health_glance( 349 - _active_capture(), None, "5m ago", brain=_ready_brain() 696 + _active_capture(), 697 + None, 698 + "5m ago", 699 + brain=_ready_brain(), 700 + backlog=healthy_backlog_source(), 350 701 ) 351 702 352 703 assert result["verdict"] == "ok" ··· 356 707 357 708 def test_checking_brain_returns_status_only_amber_row(): 358 709 result = build_health_glance( 359 - _active_capture(), None, "5m ago", brain=_checking_brain() 710 + _active_capture(), 711 + None, 712 + "5m ago", 713 + brain=_checking_brain(), 714 + backlog=healthy_backlog_source(), 360 715 ) 361 716 362 717 _assert_status_row( ··· 372 727 None, 373 728 "5m ago", 374 729 brain=_bundled_runtime_brain(progressing=True), 730 + backlog=healthy_backlog_source(), 375 731 ) 376 732 377 733 _assert_status_row( ··· 395 751 def test_no_observers_with_inflight_brain_returns_brain_status_row( 396 752 brain, verdict, headline 397 753 ): 398 - result = build_health_glance(_no_observers_capture(), None, None, brain=brain) 754 + result = build_health_glance( 755 + _no_observers_capture(), 756 + None, 757 + None, 758 + brain=brain, 759 + backlog=healthy_backlog_source(), 760 + ) 399 761 400 762 _assert_status_row(result, verdict=verdict, headline=headline) 401 763 assert result["verdict"] != "ok" ··· 404 766 405 767 @pytest.mark.parametrize("brain", [_ready_brain(), None]) 406 768 def test_no_observers_ready_or_absent_brain_keeps_setup_cta(brain): 407 - result = build_health_glance(_no_observers_capture(), None, None, brain=brain) 769 + result = build_health_glance( 770 + _no_observers_capture(), 771 + None, 772 + None, 773 + brain=brain, 774 + backlog=healthy_backlog_source(), 775 + ) 408 776 409 777 _assert_no_observers_row(result) 410 778 ··· 423 791 def test_unavailable_capture_with_inflight_brain_returns_unavailable_row( 424 792 capture_health, brain 425 793 ): 426 - result = build_health_glance(capture_health, None, None, brain=brain) 794 + result = build_health_glance( 795 + capture_health, None, None, brain=brain, backlog=healthy_backlog_source() 796 + ) 427 797 428 798 _assert_unavailable_row(result) 429 799 ··· 434 804 None, 435 805 None, 436 806 brain=_bundled_runtime_brain(progressing=True), 807 + backlog=healthy_backlog_source(), 437 808 ) 438 809 439 810 _assert_status_row( ··· 449 820 None, 450 821 None, 451 822 brain=_bundled_runtime_brain(progressing=False), 823 + backlog=healthy_backlog_source(), 452 824 ) 453 825 454 826 _assert_single_brain_issue( ··· 471 843 ], 472 844 ) 473 845 def test_actionable_brain_states_return_single_amber_issue(brain, text, href): 474 - result = build_health_glance(_active_capture(), None, None, brain=brain) 846 + result = build_health_glance( 847 + _active_capture(), None, None, brain=brain, backlog=healthy_backlog_source() 848 + ) 475 849 476 850 _assert_single_brain_issue(result, text=text, href=href) 477 851 ··· 481 855 [_checking_brain(), _bundled_runtime_brain(progressing=True)], 482 856 ) 483 857 def test_capture_and_pipeline_attention_precede_inflight_brain_status(brain): 484 - red_expected = build_health_glance(_degraded_capture("fedora"), None, None) 858 + red_expected = build_health_glance( 859 + _degraded_capture("fedora"), None, None, backlog=healthy_backlog_source() 860 + ) 485 861 red_actual = build_health_glance( 486 - _degraded_capture("fedora"), None, None, brain=brain 862 + _degraded_capture("fedora"), 863 + None, 864 + None, 865 + brain=brain, 866 + backlog=healthy_backlog_source(), 487 867 ) 488 868 489 869 assert red_actual == red_expected 490 870 491 871 pipeline = {"status": "warning", "headline": "processing needs attention"} 492 - amber_expected = build_health_glance(_active_capture(), pipeline, None) 493 - amber_actual = build_health_glance(_active_capture(), pipeline, None, brain=brain) 872 + amber_expected = build_health_glance( 873 + _active_capture(), pipeline, None, backlog=healthy_backlog_source() 874 + ) 875 + amber_actual = build_health_glance( 876 + _active_capture(), pipeline, None, brain=brain, backlog=healthy_backlog_source() 877 + ) 494 878 495 879 assert amber_actual == amber_expected 496 880 ··· 507 891 ], 508 892 ) 509 893 def test_canonical_non_ready_brain_states_do_not_return_ok_green(brain, may_be_green): 510 - result = build_health_glance(_active_capture(), None, "5m ago", brain=brain) 894 + result = build_health_glance( 895 + _active_capture(), None, "5m ago", brain=brain, backlog=healthy_backlog_source() 896 + ) 511 897 512 898 if may_be_green: 513 899 assert result["verdict"] == "ok" ··· 523 909 524 910 @pytest.mark.parametrize("brain", [None, "checking"]) 525 911 def test_absent_or_non_dict_brain_keeps_active_capture_green(brain): 526 - result = build_health_glance(_active_capture(), None, "5m ago", brain=brain) 912 + result = build_health_glance( 913 + _active_capture(), None, "5m ago", brain=brain, backlog=healthy_backlog_source() 914 + ) 527 915 528 916 assert result["verdict"] == "ok" 529 917 assert result["severity"] == "green" ··· 535 923 536 924 def test_all_issue_and_cta_hrefs_are_local_paths(): 537 925 states = [ 538 - build_health_glance(_degraded_capture("fedora"), None, None), 539 - build_health_glance({"status": "offline", "observers": []}, None, None), 926 + build_health_glance( 927 + _degraded_capture("fedora"), None, None, backlog=healthy_backlog_source() 928 + ), 929 + build_health_glance( 930 + {"status": "offline", "observers": []}, 931 + None, 932 + None, 933 + backlog=healthy_backlog_source(), 934 + ), 540 935 build_health_glance( 541 936 {"status": "stale", "observers": [{"name": "fedora", "status": "stale"}]}, 542 937 None, 543 938 None, 939 + backlog=healthy_backlog_source(), 544 940 ), 545 941 build_health_glance( 546 942 _active_capture(), ··· 550 946 "suggested_action": "open_support", 551 947 }, 552 948 None, 949 + backlog=healthy_backlog_source(), 553 950 ), 554 951 build_health_glance( 555 952 _active_capture(), 556 953 None, 557 954 None, 558 955 brain=_bundled_runtime_brain(progressing=False), 956 + backlog=healthy_backlog_source(), 559 957 ), 560 - build_health_glance({"status": "no_observers", "observers": []}, None, None), 958 + build_health_glance( 959 + {"status": "no_observers", "observers": []}, 960 + None, 961 + None, 962 + backlog=healthy_backlog_source(), 963 + ), 561 964 ] 562 965 563 966 for state in states: ··· 570 973 571 974 572 975 def test_malformed_pipeline_drops_only_pipeline_issue(): 573 - result = build_health_glance(_degraded_capture("fedora"), "warning", None) 976 + result = build_health_glance( 977 + _degraded_capture("fedora"), "warning", None, backlog=healthy_backlog_source() 978 + ) 574 979 575 980 assert result["verdict"] == "attention" 576 981 assert result["severity"] == "red" ··· 580 985 581 986 def test_owner_facing_strings_use_allowed_terms(): 582 987 states = [ 583 - build_health_glance(_degraded_capture("fedora"), None, None), 584 - build_health_glance({"status": "offline", "observers": []}, None, None), 988 + build_health_glance( 989 + _degraded_capture("fedora"), None, None, backlog=healthy_backlog_source() 990 + ), 991 + build_health_glance( 992 + {"status": "offline", "observers": []}, 993 + None, 994 + None, 995 + backlog=healthy_backlog_source(), 996 + ), 585 997 build_health_glance( 586 998 {"status": "stale", "observers": [{"name": "fedora", "status": "stale"}]}, 587 999 None, 588 1000 None, 1001 + backlog=healthy_backlog_source(), 589 1002 ), 590 - build_health_glance(_active_capture(), None, "5m ago"), 591 - build_health_glance(_active_capture(), None, None, brain=_blocked_brain()), 592 - build_health_glance(_active_capture(), None, None, brain=_checking_brain()), 1003 + build_health_glance( 1004 + _active_capture(), None, "5m ago", backlog=healthy_backlog_source() 1005 + ), 1006 + build_health_glance( 1007 + _active_capture(), 1008 + None, 1009 + None, 1010 + brain=_blocked_brain(), 1011 + backlog=healthy_backlog_source(), 1012 + ), 1013 + build_health_glance( 1014 + _active_capture(), 1015 + None, 1016 + None, 1017 + brain=_checking_brain(), 1018 + backlog=healthy_backlog_source(), 1019 + ), 593 1020 build_health_glance( 594 1021 _active_capture(), 595 1022 None, 596 1023 None, 597 1024 brain=_bundled_runtime_brain(progressing=True), 1025 + backlog=healthy_backlog_source(), 598 1026 ), 599 - build_health_glance({"status": "no_observers", "observers": []}, None, None), 600 - build_health_glance({"status": "unknown", "observers": []}, None, None), 1027 + build_health_glance( 1028 + {"status": "no_observers", "observers": []}, 1029 + None, 1030 + None, 1031 + backlog=healthy_backlog_source(), 1032 + ), 1033 + build_health_glance( 1034 + {"status": "unknown", "observers": []}, 1035 + None, 1036 + None, 1037 + backlog=healthy_backlog_source(), 1038 + ), 601 1039 build_health_glance( 602 1040 _active_capture(), 603 1041 {"status": "warning", "headline": "processing needs attention"}, 604 1042 None, 1043 + backlog=healthy_backlog_source(), 605 1044 ), 606 1045 ] 607 1046 ··· 617 1056 618 1057 619 1058 def test_brain_blocked_chip_uses_owner_copy() -> None: 620 - result = build_health_glance(_active_capture(), None, None, brain=_blocked_brain()) 1059 + result = build_health_glance( 1060 + _active_capture(), 1061 + None, 1062 + None, 1063 + brain=_blocked_brain(), 1064 + backlog=healthy_backlog_source(), 1065 + ) 621 1066 chip = next( 622 1067 issue for issue in result["issues"] if issue["text"] == HEADLINES["blocked"] 623 1068 )
+49 -1
tests/test_home_routes.py
··· 3 3 4 4 from __future__ import annotations 5 5 6 - from datetime import datetime 6 + from datetime import datetime, timezone 7 7 from pathlib import Path 8 8 from typing import Any 9 9 10 10 import pytest 11 11 12 12 from solstone.convey import create_app 13 + from solstone.convey.backlog_source import BacklogSource 13 14 from solstone.think.day_accumulator import append_record 15 + from tests.helpers.health_glance import healthy_backlog_source 14 16 15 17 16 18 def _patch_minimal_pulse_context( ··· 66 68 monkeypatch.setattr(home_routes, "_load_latest_weekly_reflection", lambda: None) 67 69 monkeypatch.setattr(home_routes, "read_steward_health", lambda: None) 68 70 monkeypatch.setattr(home_routes, "read_steward_summary", lambda *a, **k: None) 71 + monkeypatch.setattr( 72 + home_routes, 73 + "load_backlog_source", 74 + lambda _journal_root: healthy_backlog_source(), 75 + ) 69 76 monkeypatch.setattr( 70 77 home_routes, 71 78 "build_brain_snapshot", ··· 238 245 assert len(ctx["needs_you_items"]) == 1 239 246 assert ctx["briefing_needs_shared_count"] == 1 240 247 assert ctx["briefing_needs_deduped"] == [] 248 + 249 + 250 + def test_build_pulse_context_routes_loaded_backlog_signal_to_glance(monkeypatch): 251 + home_routes = _patch_minimal_pulse_context( 252 + monkeypatch, 253 + pulse_needs=[], 254 + briefing_needs=[], 255 + ) 256 + source = BacklogSource( 257 + backlog={ 258 + "pending_days": 0, 259 + "stuck_days": 1, 260 + "days": [ 261 + { 262 + "day": "20260720", 263 + "state": "stuck", 264 + "reason_code": "provider_request_rejected", 265 + "provider": "google", 266 + } 267 + ], 268 + "errors": [], 269 + "degraded": False, 270 + }, 271 + validity="valid", 272 + generated_at=datetime.now(timezone.utc).isoformat(), 273 + ) 274 + monkeypatch.setattr( 275 + home_routes, 276 + "load_backlog_source", 277 + lambda _journal_root: source, 278 + ) 279 + 280 + ctx = home_routes._build_pulse_context() 281 + 282 + assert ctx["health_glance"]["verdict"] == "attention" 283 + assert ctx["health_glance"]["severity"] == "red" 284 + assert any( 285 + "the AI provider refused a request sol sent" in issue["text"] 286 + and "Gemini" in issue["text"] 287 + for issue in ctx["health_glance"]["issues"] 288 + ) 241 289 242 290 243 291 def test_owner_voice_needs_exception_omits_owner_pair_only(monkeypatch):
+9
tests/test_home_yesterdays_processing.py
··· 30 30 _summarize_yesterday_processing, 31 31 ) 32 32 from solstone.think.brain_health import HEADLINES 33 + from tests.helpers.health_glance import healthy_backlog_source 33 34 34 35 FIXTURES = Path(__file__).parent / "fixtures" / "journal" 35 36 ··· 228 229 monkeypatch.setattr( 229 230 "solstone.apps.home.routes.read_steward_summary", 230 231 lambda *a, **k: None, 232 + ) 233 + monkeypatch.setattr( 234 + "solstone.apps.home.routes.load_backlog_source", 235 + lambda _journal_root: healthy_backlog_source(), 231 236 ) 232 237 monkeypatch.setattr( 233 238 "solstone.apps.home.routes.build_brain_snapshot", ··· 845 850 "solstone.apps.home.routes._collect_activities", lambda today: [] 846 851 ) 847 852 monkeypatch.setattr("solstone.apps.home.routes.read_steward_health", lambda: None) 853 + monkeypatch.setattr( 854 + "solstone.apps.home.routes.load_backlog_source", 855 + lambda _journal_root: healthy_backlog_source(), 856 + ) 848 857 monkeypatch.setattr( 849 858 "solstone.apps.home.routes.build_brain_snapshot", 850 859 lambda *_a, **_k: _brain_snapshot(),