"""Guard: every ``pending_questions`` ``kind`` has a coordinator delivery path. A human-gate interrupt ``kind`` is NOT delivered for free. The responder/notify path is not kind-agnostic: a ``kind`` added to the ``pending_questions`` CHECK without a coordinator branch makes the task suspend FOREVER — no Slack post, no durable ledger row, the human can never answer. That was a real CRITICAL gap when ``confluence_approval`` was first wired (the node + graph were complete but the coordinator only branched on ``plan_decision``). See the memory ``reference_agent_team_extension_gotchas``. This test pins the invariant from the schema (the single source of truth for allowed kinds): every non-default kind must have a ``*_KIND`` constant in ``agent_team.graph`` AND an explicit reference to it in ``coordinator.py`` (its delivery branch). A new kind therefore cannot ship without its delivery wiring. """ from __future__ import annotations import re from pathlib import Path from agent_team import coordinator as coordinator_mod from agent_team import graph as graph_mod from agent_team.db import schema as schema_mod # ``clarify`` is delivered by the DEFAULT notify path (the fallthrough in # ``_post_resume_followups`` / ``start_task``), so it needs no dedicated branch. # Every OTHER kind must be keyed on its own ``graph.*_KIND`` constant. _DEFAULT_DELIVERED_KINDS = {"clarify"} def _declared_kinds() -> set[str]: """The kind values the ``pending_questions`` CHECK allows (source of truth).""" match = re.search(r"kind IN \(([^)]*)\)", schema_mod.PENDING_QUESTIONS_DDL) assert match, ( "could not find the pending_questions kind CHECK in PENDING_QUESTIONS_DDL" ) return set(re.findall(r"'([^']+)'", match.group(1))) def _kind_constant_names() -> dict[str, str]: """Map each kind VALUE to the ``graph.*_KIND`` constant NAME that holds it.""" out: dict[str, str] = {} for name in dir(graph_mod): if name.endswith("_KIND"): value = getattr(graph_mod, name) if isinstance(value, str): out[value] = name return out def test_declared_kinds_parse_sane() -> None: # Guards the regex itself: if PENDING_QUESTIONS_DDL is reshaped and the CHECK # stops parsing, this fails loudly instead of silently passing an empty set. kinds = _declared_kinds() assert {"clarify", "plan_decision"} <= kinds def test_every_pending_question_kind_has_coordinator_delivery() -> None: kinds = _declared_kinds() constants = _kind_constant_names() coordinator_src = Path(coordinator_mod.__file__).read_text() for kind in sorted(kinds - _DEFAULT_DELIVERED_KINDS): const = constants.get(kind) assert const, ( f"pending_questions kind {kind!r} has no `*_KIND` constant in " "agent_team.graph — define one and give it a coordinator delivery " "branch (see reference_agent_team_extension_gotchas)." ) assert f"graph_mod.{const}" in coordinator_src, ( f"pending_questions kind {kind!r} ({const}) has no delivery branch in " "coordinator.py — a kind added to the CHECK without a coordinator " "branch suspends the task forever (no Slack post, no ledger row). Add " "a branch keyed on graph_mod." + (const or "") + " in " "_post_resume_followups, start_task, AND the redelivery sweep, and " "make the interrupt payload carry a question_set " "(see reference_agent_team_extension_gotchas)." )