This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/tests/test_responder.py
Adam Moussa 15a416d31a Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.

Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled

KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven

Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 15:16:12 -04:00

574 lines
18 KiB
Python

"""Unit tests for agent_team.responder (§3.3, §3.3.1).
Covers the notify+resume seam end to end against the real ``pending_questions``
ledger (an on-disk SQLite DB via the foundation ``init_db``/``connect``):
* notify: ledger-row-first ordering, channel_ref persisted, lost-post leaves an
open row with no ref;
* submit_answer: first-answer-wins accept + enqueue, duplicate/late no-op,
answer-after-expiry loses the compare-and-set;
* ResumeWorker: turn-guard skip → superseded, happy-path resume, single-flight
per-thread serialization, concurrent different threads;
* deadline_sweep: overdue open → expired, race vs answer;
* recover_open_questions: answered rows re-enqueued.
"""
from __future__ import annotations
import sqlite3
import threading
import time
from pathlib import Path
from typing import Any
import pytest
from agent_team.db.schema import (
answer_question,
connect,
expire_question,
init_db,
)
from agent_team.responder import (
AnswerOutcome,
GraphHandle,
ResumeJob,
ResumeWorker,
deadline_sweep,
notify_question,
recover_open_questions,
submit_answer,
)
from agent_team.transport.base import NormalizedAnswer, QuestionSet, Transport
# ---------------------------------------------------------------------------
# Fixtures + fakes.
# ---------------------------------------------------------------------------
@pytest.fixture
def conn(tmp_path: Path) -> sqlite3.Connection:
"""A real ledger-backed connection (foundation schema)."""
db_path = tmp_path / "agent-team.db"
init_db(db_path)
connection = connect(db_path)
try:
yield connection
finally:
connection.close()
class FakeTransport(Transport):
"""In-memory transport recording posts and parsing dict answers.
``post_question`` embeds the ``question_id`` in the returned ref (Slack
``ts`` analogue). ``post_fails`` toggles the lost-post path.
"""
def __init__(self, *, post_fails: bool = False) -> None:
self.posts: list[dict[str, Any]] = []
self.post_fails = post_fails
def post_question(
self, *, thread_id, question_id, turn, question_set, deadline
) -> str:
if self.post_fails:
raise RuntimeError("transport unreachable")
ref = f"slack-ts-{question_id}"
self.posts.append(
{
"thread_id": thread_id,
"question_id": question_id,
"turn": turn,
"deadline": deadline,
"ref": ref,
}
)
return ref
def parse_answer(self, raw) -> tuple[str, Any, str]:
na = NormalizedAnswer(
question_id=raw["callback_id"], answer=raw["value"], via="slack"
)
return na.question_id, na.answer, na.via
class FakeGraph:
"""Structural ``GraphHandle``: configurable interrupt turn + resume recorder."""
def __init__(self, turns: dict[str, int | None] | None = None) -> None:
# thread_id -> turn it is interrupted on (None = not interrupted).
self.turns: dict[str, int | None] = turns or {}
self.resumed: list[tuple[str, Any]] = []
self._resume_hook = None
def interrupted_turn(self, thread_id: str) -> int | None:
return self.turns.get(thread_id)
def resume(self, thread_id: str, answer: Any) -> Any:
if self._resume_hook is not None:
self._resume_hook(thread_id)
self.resumed.append((thread_id, answer))
return {"resumed": thread_id}
def _question_set(
*, thread_id: str = "t1", question_id: str = "q1", turn: int = 0
) -> QuestionSet:
return QuestionSet(
thread_id=thread_id,
question_id=question_id,
turn=turn,
questions=["proceed?"],
context={"repo": "x"},
)
def _row(conn: sqlite3.Connection, qid: str) -> sqlite3.Row:
return conn.execute(
"SELECT * FROM pending_questions WHERE question_id=?", (qid,)
).fetchone()
# ---------------------------------------------------------------------------
# Protocol conformance.
# ---------------------------------------------------------------------------
def test_fake_graph_satisfies_protocol() -> None:
assert isinstance(FakeGraph(), GraphHandle)
# ---------------------------------------------------------------------------
# notify_question — delivery + lost-post (§3.3.1).
# ---------------------------------------------------------------------------
def test_notify_writes_open_row_then_stores_ref(conn: sqlite3.Connection) -> None:
transport = FakeTransport()
qs = _question_set()
ref = notify_question(conn, transport, qs, deadline="2026-06-18T00:00:00+00:00")
assert ref == "slack-ts-q1"
row = _row(conn, "q1")
assert row["status"] == "open"
assert row["channel_ref"] == "slack-ts-q1"
assert row["thread_id"] == "t1"
assert row["turn"] == 0
assert row["transport"] == "FakeTransport"
assert row["deadline_at"] == "2026-06-18T00:00:00+00:00"
assert row["posted_at"] is not None
# The post carried the question_id so an answer can map back.
assert transport.posts[0]["question_id"] == "q1"
def test_notify_row_is_written_before_post(conn: sqlite3.Connection) -> None:
"""The durable row must exist even while the post is in flight."""
seen: dict[str, Any] = {}
class CheckingTransport(FakeTransport):
def post_question(self, *, question_id, **kw): # type: ignore[override]
# At post time the ledger row must already be persisted as open.
seen["row"] = _row(conn, question_id)
return super().post_question(question_id=question_id, **kw)
notify_question(
conn, CheckingTransport(), _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
assert seen["row"] is not None
assert seen["row"]["status"] == "open"
def test_notify_lost_post_leaves_open_row_without_ref(
conn: sqlite3.Connection,
) -> None:
transport = FakeTransport(post_fails=True)
ref = notify_question(
conn, transport, _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
# Post failed: no ref returned, row stays open with no ref for reconcile.
assert ref is None
row = _row(conn, "q1")
assert row["status"] == "open"
assert row["channel_ref"] is None
# ---------------------------------------------------------------------------
# submit_answer — first-answer-wins (§3.3.1).
# ---------------------------------------------------------------------------
def test_submit_answer_first_wins_enqueues_resume(
conn: sqlite3.Connection,
) -> None:
transport = FakeTransport()
notify_question(
conn, transport, _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
enqueued: list[ResumeJob] = []
outcome = submit_answer(
conn,
transport,
{"callback_id": "q1", "value": "yes"},
enqueue_resume=enqueued.append,
)
assert isinstance(outcome, AnswerOutcome)
assert outcome.accepted is True
assert outcome.question_id == "q1"
assert outcome.via == "slack"
assert outcome.job is not None
assert outcome.job == ResumeJob(
thread_id="t1", question_id="q1", turn=0, answer="yes"
)
assert enqueued == [outcome.job]
row = _row(conn, "q1")
assert row["status"] == "answered"
assert row["answer_json"] == '"yes"'
assert row["answered_via"] == "slack"
assert row["answered_at"] is not None
def test_submit_answer_duplicate_is_noop(conn: sqlite3.Connection) -> None:
transport = FakeTransport()
notify_question(
conn, transport, _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
enqueued: list[ResumeJob] = []
first = submit_answer(
conn,
transport,
{"callback_id": "q1", "value": "yes"},
enqueue_resume=enqueued.append,
)
second = submit_answer(
conn,
transport,
{"callback_id": "q1", "value": "no"},
enqueue_resume=enqueued.append,
)
assert first.accepted is True
assert second.accepted is False
assert second.job is None
# Only the first answer enqueued a resume; the duplicate is ignored.
assert len(enqueued) == 1
# The stored answer is the first one, never overwritten by the duplicate.
assert _row(conn, "q1")["answer_json"] == '"yes"'
def test_submit_answer_after_expiry_loses_race(conn: sqlite3.Connection) -> None:
transport = FakeTransport()
notify_question(
conn, transport, _question_set(), deadline="2000-01-01T00:00:00+00:00"
)
# Question times out first.
assert expire_question(conn, question_id="q1") is True
enqueued: list[ResumeJob] = []
outcome = submit_answer(
conn,
transport,
{"callback_id": "q1", "value": "yes"},
enqueue_resume=enqueued.append,
)
assert outcome.accepted is False
assert enqueued == []
assert _row(conn, "q1")["status"] == "expired"
def test_submit_answer_unknown_question_is_noop(conn: sqlite3.Connection) -> None:
transport = FakeTransport()
enqueued: list[ResumeJob] = []
outcome = submit_answer(
conn,
transport,
{"callback_id": "nope", "value": "x"},
enqueue_resume=enqueued.append,
)
assert outcome.accepted is False
assert enqueued == []
# ---------------------------------------------------------------------------
# ResumeWorker — single-flight, turn-guarded (§3.3.1).
# ---------------------------------------------------------------------------
def test_resume_happy_path(conn: sqlite3.Connection) -> None:
notify_question(
conn, FakeTransport(), _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
answer_question(conn, question_id="q1", answer_json='"yes"', answered_via="slack")
graph = FakeGraph(turns={"t1": 0})
worker = ResumeWorker(conn, graph)
job = ResumeJob(thread_id="t1", question_id="q1", turn=0, answer="yes")
assert worker.run(job) is True
assert graph.resumed == [("t1", "yes")]
# Still 'answered' — the worker does not mutate the ledger on success.
assert _row(conn, "q1")["status"] == "answered"
def test_resume_stale_turn_supersedes_and_skips(conn: sqlite3.Connection) -> None:
notify_question(
conn,
FakeTransport(),
_question_set(turn=2),
deadline="2026-06-18T00:00:00+00:00",
)
answer_question(conn, question_id="q1", answer_json='"yes"', answered_via="slack")
# Graph already advanced to turn 3 (or is not on turn 2 any more).
graph = FakeGraph(turns={"t1": 3})
worker = ResumeWorker(conn, graph)
job = ResumeJob(thread_id="t1", question_id="q1", turn=2, answer="yes")
assert worker.run(job) is False
assert graph.resumed == []
assert _row(conn, "q1")["status"] == "superseded"
def test_resume_not_interrupted_supersedes_and_skips(
conn: sqlite3.Connection,
) -> None:
notify_question(
conn, FakeTransport(), _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
answer_question(conn, question_id="q1", answer_json='"yes"', answered_via="slack")
# Thread not currently interrupted (None).
graph = FakeGraph(turns={"t1": None})
worker = ResumeWorker(conn, graph)
job = ResumeJob(thread_id="t1", question_id="q1", turn=0, answer="yes")
assert worker.run(job) is False
assert graph.resumed == []
assert _row(conn, "q1")["status"] == "superseded"
def test_resume_redelivered_job_does_not_double_apply(
conn: sqlite3.Connection,
) -> None:
"""A second (redelivered) job for the same answered turn supersedes-skips."""
notify_question(
conn, FakeTransport(), _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
answer_question(conn, question_id="q1", answer_json='"yes"', answered_via="slack")
graph = FakeGraph(turns={"t1": 0})
worker = ResumeWorker(conn, graph)
job = ResumeJob(thread_id="t1", question_id="q1", turn=0, answer="yes")
assert worker.run(job) is True
# The real graph advances after a successful resume; model that.
graph.turns["t1"] = 1
assert worker.run(job) is False
# Resume applied exactly once.
assert graph.resumed == [("t1", "yes")]
def test_resume_single_flight_serializes_same_thread(
conn: sqlite3.Connection,
) -> None:
"""Two jobs for one thread never resume concurrently (per-thread lock)."""
notify_question(
conn, FakeTransport(), _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
answer_question(conn, question_id="q1", answer_json='"yes"', answered_via="slack")
graph = FakeGraph(turns={"t1": 0})
concurrency = {"current": 0, "max": 0}
lock = threading.Lock()
def hook(_thread_id: str) -> None:
with lock:
concurrency["current"] += 1
concurrency["max"] = max(concurrency["max"], concurrency["current"])
time.sleep(0.02)
with lock:
concurrency["current"] -= 1
graph._resume_hook = hook
worker = ResumeWorker(conn, graph)
job = ResumeJob(thread_id="t1", question_id="q1", turn=0, answer="yes")
threads = [threading.Thread(target=worker.run, args=(job,)) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
# Same thread_id => never more than one in-flight resume at a time.
assert concurrency["max"] == 1
def test_resume_different_threads_run_concurrently(
conn: sqlite3.Connection,
) -> None:
"""Different thread_ids are NOT serialized against each other."""
graph = FakeGraph(turns={f"t{i}": 0 for i in range(4)})
barrier = threading.Barrier(4, timeout=2.0)
reached = {"ok": True}
def hook(_thread_id: str) -> None:
try:
barrier.wait()
except threading.BrokenBarrierError:
reached["ok"] = False
graph._resume_hook = hook
worker = ResumeWorker(conn, graph)
jobs = [
ResumeJob(thread_id=f"t{i}", question_id=f"q{i}", turn=0, answer="y")
for i in range(4)
]
threads = [threading.Thread(target=worker.run, args=(j,)) for j in jobs]
for t in threads:
t.start()
for t in threads:
t.join()
# All four reached the barrier together => they ran concurrently.
assert reached["ok"] is True
assert len(graph.resumed) == 4
# ---------------------------------------------------------------------------
# deadline_sweep — overdue open -> expired (§3.3.1).
# ---------------------------------------------------------------------------
def test_deadline_sweep_expires_only_overdue_open(
conn: sqlite3.Connection,
) -> None:
transport = FakeTransport()
notify_question(
conn,
transport,
_question_set(question_id="overdue"),
deadline="2000-01-01T00:00:00+00:00",
)
notify_question(
conn,
transport,
_question_set(question_id="future"),
deadline="2099-01-01T00:00:00+00:00",
)
expired = deadline_sweep(conn, now="2026-06-17T00:00:00+00:00")
assert expired == ["overdue"]
assert _row(conn, "overdue")["status"] == "expired"
assert _row(conn, "future")["status"] == "open"
def test_deadline_sweep_skips_already_answered(
conn: sqlite3.Connection,
) -> None:
transport = FakeTransport()
notify_question(
conn,
transport,
_question_set(question_id="ans"),
deadline="2000-01-01T00:00:00+00:00",
)
answer_question(conn, question_id="ans", answer_json='"yes"', answered_via="slack")
expired = deadline_sweep(conn, now="2026-06-17T00:00:00+00:00")
# Already answered => the deadline race was already lost; not expired.
assert expired == []
assert _row(conn, "ans")["status"] == "answered"
def test_deadline_sweep_ignores_null_deadline(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO pending_questions "
"(question_id, thread_id, turn, status, transport) "
"VALUES ('q', 't', 0, 'open', 'slack')"
)
assert deadline_sweep(conn, now="2099-01-01T00:00:00+00:00") == []
assert _row(conn, "q")["status"] == "open"
# ---------------------------------------------------------------------------
# recover_open_questions — startup answered->resume replay (§3.3.1).
# ---------------------------------------------------------------------------
def test_recover_reenqueues_answered_rows(conn: sqlite3.Connection) -> None:
transport = FakeTransport()
notify_question(
conn,
transport,
_question_set(thread_id="ta", question_id="qa"),
deadline="2026-06-18T00:00:00+00:00",
)
notify_question(
conn,
transport,
_question_set(thread_id="tb", question_id="qb", turn=1),
deadline="2026-06-18T00:00:00+00:00",
)
notify_question(
conn,
transport,
_question_set(thread_id="tc", question_id="qc"),
deadline="2026-06-18T00:00:00+00:00",
)
# qa, qb answered before a crash; qc still open.
answer_question(conn, question_id="qa", answer_json='"yes"', answered_via="slack")
answer_question(
conn, question_id="qb", answer_json='{"k": 1}', answered_via="github"
)
enqueued: list[ResumeJob] = []
jobs = recover_open_questions(conn, enqueue_resume=enqueued.append)
assert jobs == enqueued
by_thread = {j.thread_id: j for j in jobs}
assert set(by_thread) == {"ta", "tb"}
assert by_thread["ta"] == ResumeJob(
thread_id="ta", question_id="qa", turn=0, answer="yes"
)
assert by_thread["tb"] == ResumeJob(
thread_id="tb", question_id="qb", turn=1, answer={"k": 1}
)
def test_recover_is_idempotent_via_turn_guard(conn: sqlite3.Connection) -> None:
"""Re-enqueued recover jobs no-op when the graph already advanced."""
notify_question(
conn, FakeTransport(), _question_set(), deadline="2026-06-18T00:00:00+00:00"
)
answer_question(conn, question_id="q1", answer_json='"yes"', answered_via="slack")
enqueued: list[ResumeJob] = []
recover_open_questions(conn, enqueue_resume=enqueued.append)
assert len(enqueued) == 1
# Graph already past turn 0 (resume happened before the crash record cleared).
graph = FakeGraph(turns={"t1": 1})
worker = ResumeWorker(conn, graph)
assert worker.run(enqueued[0]) is False
assert graph.resumed == []
assert _row(conn, "q1")["status"] == "superseded"
def test_recover_empty_ledger(conn: sqlite3.Connection) -> None:
enqueued: list[ResumeJob] = []
assert recover_open_questions(conn, enqueue_resume=enqueued.append) == []
assert enqueued == []