diff --git a/CHANGELOG.md b/CHANGELOG.md index 75723dbd..5c0cc230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,9 @@ All notable changes to vouch are documented here. Format follows KB under `eval/fixture-kb/`, and an `eval` workflow gating retrieval changes (#226). ### Fixed +- `review-ui` health checks now stay alive when a pending proposal YAML file is + corrupt or contains invalid control characters, so desktop launchers do not + time out waiting for `/healthz`. - `parse_since` (the `--since` parser behind `vouch metrics`/`vouch audit`) now raises a clean `MetricsError` for a duration too large to represent (e.g. `--since 1000000000000d`), instead of letting an uncaught `OverflowError` traceback escape — restoring the documented "clean error, not a traceback" contract. - `sync_apply` now loads the sync source exactly once and passes the same `_SyncSource` instance into `sync_check`, closing a TOCTOU window where a bundle replaced on disk between the two `_load_source` calls could cause the validation and write phases to operate on different snapshots. Also eliminates redundant directory walks (KB sources) and triple tarball opens (bundle sources). Fixes #217. - `vault_to_kb` now passes `slug_hint=page_id` to `propose_page` so vault edit proposals target the existing page id from frontmatter instead of a slugified copy of the title (fixes #219). diff --git a/src/vouch/web/server.py b/src/vouch/web/server.py index 2473fadb..e94c078e 100644 --- a/src/vouch/web/server.py +++ b/src/vouch/web/server.py @@ -201,6 +201,11 @@ def _pending_page(store: KBStore, page: int, page_size: int return proposals, page, pages, total +def _pending_count(store: KBStore) -> int: + proposed_dir = store.kb_dir / "proposed" + return len(list(proposed_dir.glob("*.yaml"))) if proposed_dir.is_dir() else 0 + + # --- auth ----------------------------------------------------------------- @@ -369,7 +374,7 @@ def healthz() -> dict[str, Any]: return { "ok": True, "kb": str(store.root), - "pending": len(store.list_proposals(ProposalStatus.PENDING)), + "pending": _pending_count(store), "auth": auth.enabled, "clients": hub.client_count, } diff --git a/tests/test_web.py b/tests/test_web.py index e44a7fc8..893887b3 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -239,6 +239,22 @@ def test_healthz(client: TestClient, store: KBStore) -> None: assert body["pending"] == 1 +def test_healthz_survives_unreadable_pending_yaml( + client: TestClient, store: KBStore, +) -> None: + (store.kb_dir / "proposed" / "bad.yaml").write_text( + "id: bad\npayload: '\x80'\n", + encoding="utf-8", + ) + + r = client.get("/healthz") + + assert r.status_code == 200 + body = r.json() + assert body["ok"] is True + assert body["pending"] == 1 + + # --- bring-up errors ------------------------------------------------------