From 00917696ddd70af57f0051ffe150c35e553445b9 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 8 Jul 2026 13:19:06 +0200 Subject: [PATCH] Add todo_md_staleness State Hub context resolver Supports daily TODO.md stale-review ActivityDefinition via GET /repos/todo-md-staleness. --- .../context_resolvers/state_hub.py | 22 +++++++++ tests/test_state_hub_context_resolver.py | 49 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/activity_core/context_resolvers/state_hub.py b/src/activity_core/context_resolvers/state_hub.py index f535bd5..a598c25 100644 --- a/src/activity_core/context_resolvers/state_hub.py +++ b/src/activity_core/context_resolvers/state_hub.py @@ -100,6 +100,8 @@ class StateHubContextResolver(ContextResolver): return _fetch_json(f"/state/domain/{domain}") if query == "repo_sbom_status": return _repo_sbom_status(params) + if query == "todo_md_staleness": + return _todo_md_staleness(params) if query == "state_summary": return _fetch_json("/state/summary") if query == "next_steps": @@ -374,6 +376,26 @@ def _repo_sbom_status(params: dict[str, Any]) -> dict[str, Any]: return {} +def _todo_md_staleness(params: dict[str, Any]) -> dict[str, Any]: + """Resolve TODO.md staleness from State Hub. + + Bulk mode (default): GET /repos/todo-md-staleness?stale_days=N + Returns stale repos under `repos` plus summary fields for rule matching. + """ + stale_days = params.get("stale_days", 6) + try: + stale_days = int(stale_days) + except (TypeError, ValueError): + stale_days = 6 + payload = _fetch_json("/repos/todo-md-staleness", {"stale_days": stale_days}) + if not isinstance(payload, dict) or not payload: + return {} + repos = payload.get("repos") + if not isinstance(repos, list): + return {} + return payload + + def _sbom_status_entry(repo_slug: str, last_sbom_at: Any) -> dict[str, Any]: age_days, has_sbom, normalised = _sbom_age_days(last_sbom_at) return { diff --git a/tests/test_state_hub_context_resolver.py b/tests/test_state_hub_context_resolver.py index 9458cbf..a15a5ae 100644 --- a/tests/test_state_hub_context_resolver.py +++ b/tests/test_state_hub_context_resolver.py @@ -192,6 +192,55 @@ def test_repo_sbom_status_bulk_returns_worst_repo(monkeypatch) -> None: assert by_slug["never-scanned"]["last_sbom_at"] is None +def test_todo_md_staleness_returns_stale_repos(monkeypatch) -> None: + calls: list[dict[str, Any]] = [] + + def fake_get(url: str, **kwargs: Any) -> DummyResponse: + calls.append({"url": url, **kwargs}) + return DummyResponse({ + "stale_days": 6, + "repos": [ + { + "repo_slug": "markitect-main", + "has_todo": True, + "age_days": 12, + "mtime": "2026-06-26T10:00:00+00:00", + "todo_path": "/home/worsch/markitect-main/TODO.md", + } + ], + "stale_count": 1, + "total_with_todo": 1, + "total_scanned": 62, + "worst_repo_slug": "markitect-main", + "worst_age_days": 12, + }) + + monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test") + monkeypatch.setattr(httpx, "get", fake_get) + + result = StateHubContextResolver().resolve( + "todo_md_staleness", None, {"stale_days": 6} + ) + + assert calls == [{ + "url": "http://state-hub.test/repos/todo-md-staleness", + "params": {"stale_days": 6}, + "timeout": 10.0, + }] + assert result["stale_count"] == 1 + assert result["repos"][0]["repo_slug"] == "markitect-main" + + +def test_todo_md_staleness_returns_empty_on_failure(monkeypatch) -> None: + def fake_get(url: str, **kwargs: Any) -> DummyResponse: + return DummyResponse(None, status_error=httpx.HTTPError("boom")) + + monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test") + monkeypatch.setattr(httpx, "get", fake_get) + + assert StateHubContextResolver().resolve("todo_md_staleness", None, {}) == {} + + def test_repo_sbom_status_returns_empty_on_failure(monkeypatch) -> None: def fake_get(url: str, **kwargs: Any) -> DummyResponse: return DummyResponse(None, status_error=httpx.HTTPError("boom"))