From 9a72c9f2103b5db897585ec878bd0ce17cfee3ae Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 18 Jun 2026 08:11:09 +0200 Subject: [PATCH] fix: unwrap single-key kaizen resolver payloads in resolve_context When discover_kaizen_projects returns {"projects": [...]} bound to context.projects, for_each can iterate the list directly. Multi-key summaries (e.g. repo SBOM bulk) remain unchanged. --- src/activity_core/activities.py | 16 +++++++++- tests/test_resolve_context_binding.py | 45 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/test_resolve_context_binding.py diff --git a/src/activity_core/activities.py b/src/activity_core/activities.py index b662a45..19e0bfb 100644 --- a/src/activity_core/activities.py +++ b/src/activity_core/activities.py @@ -13,6 +13,7 @@ from __future__ import annotations import uuid from datetime import datetime, timezone +from typing import Any from sqlalchemy import select from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -52,6 +53,18 @@ def _get_session_factory() -> async_sessionmaker[AsyncSession]: return _session_factory +def _bind_resolver_result(bind_key: str, result: Any) -> Any: + """Unwrap single-key resolver payloads when the key matches bind_key. + + Resolvers such as ``discover_kaizen_projects`` return ``{"projects": [...]}`` + while definitions bind to ``context.projects`` and iterate ``for_each: + context.projects``. Multi-key summaries (e.g. repo SBOM bulk) stay intact. + """ + if isinstance(result, dict) and len(result) == 1 and bind_key in result: + return result[bind_key] + return result + + # ── Activities ───────────────────────────────────────────────────────────────── @activity.defn @@ -139,7 +152,8 @@ async def resolve_context( continue try: - snapshot[bind_key] = resolver_cls().resolve(query, None, params) + resolved = resolver_cls().resolve(query, None, params) + snapshot[bind_key] = _bind_resolver_result(bind_key, resolved) except Exception as exc: if required: raise ApplicationError( diff --git a/tests/test_resolve_context_binding.py b/tests/test_resolve_context_binding.py new file mode 100644 index 0000000..74790f3 --- /dev/null +++ b/tests/test_resolve_context_binding.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import pytest + +from activity_core.activities import _bind_resolver_result, resolve_context + + +def test_bind_resolver_result_unwraps_single_key_wrapper() -> None: + projects = [{"repo": "kaizen-agentic", "has_metrics": True}] + assert _bind_resolver_result("projects", {"projects": projects}) == projects + + +def test_bind_resolver_result_keeps_multi_key_summary() -> None: + summary = { + "repos": [{"repo_slug": "a"}], + "stale_count": 1, + "total_count": 2, + } + assert _bind_resolver_result("repos", summary) == summary + + +@pytest.mark.asyncio +async def test_resolve_context_unwraps_kaizen_projects(monkeypatch) -> None: + class _FakeResolver: + def resolve(self, query: str, event: object, params: dict) -> dict: + assert query == "discover_kaizen_projects" + return {"projects": [{"repo": "pilot", "has_metrics": True}]} + + import activity_core.context_resolvers # noqa: F401 + from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY + + monkeypatch.setitem(CONTEXT_RESOLVER_REGISTRY, "kaizen", lambda: _FakeResolver()) + + snapshot = await resolve_context( + [ + { + "type": "kaizen", + "query": "discover_kaizen_projects", + "params": {}, + "bind_to": "context.projects", + } + ] + ) + + assert snapshot == {"projects": [{"repo": "pilot", "has_metrics": True}]} \ No newline at end of file