## Implementation Plan ### 1. Modify `NewsService` fetch with retry, audit, and notification **File:** `devplacepy/services/news/service.py` - **Import `retry_send`** from `devplacepy/services/openai_gateway/reliability.py` - **Add config fields** (near existing `ConfigField` definitions): - `news_fetch_retries` – type `int`, default `3`, min `0`, max `10`, group `"Reliability"` - `news_fetch_retry_backoff_ms` – type `int`, default `5000`, min `1000`, max `60000`, group `"Reliability"` - **Replace the bare `client.get()` call** (around line 170) with: ```python response, exc, attempts = await retry_send( do_call=lambda: client.get(api_url, timeout=timeout), max_retries=self.get_setting("news_fetch_retries"), backoff_ms=self.get_setting("news_fetch_retry_backoff_ms"), log=lambda msg: self.log(msg), ) ``` - **On failure (all retries exhausted):** - Call `audit.record_system("news.service.fetch_failed", ...)` – include `api_url` and `attempts`. - Call `_notify_admins` (implement inline or as a helper) using `create_notification` from `devplacepy/utils/notifications.py`: ```python for admin in get_table("users").find(role="Admin"): create_notification( admin["uid"], "system", f"News API unreachable after {attempts} attempts", related_uid="", target_url="", ) ``` - Log a final warning. ### 2. Update tests **File:** `tests/unit/services/news.py` - Modify `FailingApiClient` to track call count: ```python class FailingApiClient(FakeClient_news_service): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.call_count = 0 async def get(self, url, timeout=None): self.call_count += 1 if url == API_URL: raise httpx.HTTPError("api down") return FakeResp_news_service(text="") ``` - Update `test_run_once_handles_api_failure` to assert that `call_count == news_fetch_retries + 1` (default 4). Use `monkeypatch` to set the new config fields. - Add a new test `test_run_once_handles_api_failure_zero_retries` that sets `news_fetch_retries=0` and asserts `call_count == 1`. - Mock `create_notification` (or `get_table`) to verify notification is called exactly once per admin on failure. ### 3. Verify audit event key registration No change needed – `categories.py` line 27 already registers `"news"` as a prefix, so `news.service.fetch_failed` is valid. ### 4. Ensure consistency with existing patterns - The `retry_send` function from `reliability.py` uses linear backoff (backoff_ms * attempt). This is acceptable for the stated use case. - The admin notification pattern mirrors `devplacepy/routers/issues/comment.py` exactly. --- ## Definition of Done - [ ] **All code changes** as described above are present and committed. - [ ] **Lint passes:** `pip install -q ruff && ruff check .` returns exit code 0. - [ ] **Unit tests pass:** `pip install -e '.[dev]' -q && make test-unit` returns exit code 0. - [ ] **New tests pass** and include: - Assertion that `FailingApiClient.call_count == max_retries + 1` on persistent failure. - Assertion that `create_notification` is called for each admin on failure (or at least once). - Edge case with `news_fetch_retries=0` (single attempt, no retries). - [ ] **Manual verification** (optional but recommended): run `run_once()` with a mock API that fails; observe three retries, then an audit event and notification in log/database.