feat(agent-team): Confluence-writer node (draft → approve gate → dry-run write) [flag-gated] #66
1 changed files with 79 additions and 0 deletions
79
agent-team/tests/test_gate_kind_coverage.py
Normal file
79
agent-team/tests/test_gate_kind_coverage.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""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 "<KIND>") + " 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)."
|
||||
)
|
||||
Reference in a new issue