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/sim/test_p1_exit_criteria.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

391 lines
15 KiB
Python

"""P1 exit-criteria simulation tests (design §7.1 P1, demonstrating §3.3.1).
Phase P1 may begin only once the durable human-in-the-loop suspend/resume
mechanic is *demonstrated*. §7.1 P1 lists four exit criteria; this module is
the executable demonstration of each, driving the committed foundation
(:mod:`agent_team.db.schema` compare-and-set helpers + the atomic,
integrity-checked :mod:`agent_team.state_store`) through the
:mod:`harness.SimPipeline`:
* (a) kill the box mid-wait and have the task resume after restart;
* (b) submit a duplicate answer and confirm it no-ops;
* (c) submit an answer after the deadline expired and confirm it is rejected
and the task parked;
* (d) two tasks suspended concurrently resume independently to the correct
thread.
Each criterion has its own test (and a couple of supporting tests for the
delivery/recovery edges §3.3.1 calls out). The tests assert on the *durable*
state — the ledger row status and the integrity-checked task record — so they
verify the real mechanic, not a harness convenience.
"""
from __future__ import annotations
import json
import sqlite3
import threading
import pytest
from harness import (
PostFailingTransport,
RecordingTransport,
SimClock,
SimPipeline,
)
from agent_team.state_store import IntegrityError
from agent_team.task_model import Phase, TaskStatus
# ---------------------------------------------------------------------------
# Baseline: a single happy-path suspend/resume cycle.
# ---------------------------------------------------------------------------
def test_submit_suspends_task_with_open_ledger_row(
pipeline: SimPipeline, transport: RecordingTransport
) -> None:
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
record = pipeline.load_record(suspended.thread_id)
assert record.status is TaskStatus.WAITING_HUMAN
assert record.current_phase is Phase.CLARIFY
row = pipeline.ledger_row(suspended.question_id)
assert row is not None
assert row["status"] == "open"
assert row["thread_id"] == suspended.thread_id
# Delivery happened: a channel_ref was stored and it embeds the question id.
assert row["channel_ref"] == f"sim:{suspended.question_id}"
assert (
transport.posts and transport.posts[0]["question_id"] == suspended.question_id
)
def test_first_answer_wins_and_resumes_to_plan(pipeline: SimPipeline) -> None:
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
won = pipeline.submit_answer(
{"question_id": suspended.question_id, "answer": "core-api", "via": "slack:U1"}
)
assert won is True
outcome = pipeline.resume(suspended.thread_id, suspended.question_id)
assert outcome.resumed is True
assert outcome.new_phase is Phase.PLAN
record = pipeline.load_record(suspended.thread_id)
assert record.status is TaskStatus.ACTIVE
assert record.current_phase is Phase.PLAN
# The won answer was durably folded into the Q&A history.
assert record.qa_history == [
{"question_id": suspended.question_id, "turn": 0, "answer": "core-api"}
]
assert pipeline.ledger_row(suspended.question_id)["status"] == "answered"
# ---------------------------------------------------------------------------
# (a) kill the box mid-wait and have the task resume after restart.
# ---------------------------------------------------------------------------
def test_a_restart_mid_wait_then_answer_and_resume(
pipeline: SimPipeline, transport: RecordingTransport
) -> None:
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
# "Kill the box": drop the in-memory pipeline; rebuild purely from disk.
reopened = pipeline.reopen()
# Durable state survived: ledger row still open, record still WAITING_HUMAN.
row = reopened.ledger_row(suspended.question_id)
assert row is not None and row["status"] == "open"
record = reopened.load_record(suspended.thread_id)
assert record.status is TaskStatus.WAITING_HUMAN
# The human answers after the restart; the task converges via the sweep.
assert reopened.submit_answer(
{"question_id": suspended.question_id, "answer": "core-api", "via": "slack:U1"}
)
summary = reopened.startup_sweep(
questions_by_qid={suspended.question_id: ["which repo?"]}
)
assert summary["resumed"] == [suspended.thread_id]
resumed_record = reopened.load_record(suspended.thread_id)
assert resumed_record.status is TaskStatus.ACTIVE
assert resumed_record.current_phase is Phase.PLAN
def test_a_restart_after_answer_recovers_via_startup_sweep(
pipeline: SimPipeline,
) -> None:
"""An answer that won *before* the crash must still resume on restart."""
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
assert pipeline.submit_answer(
{"question_id": suspended.question_id, "answer": "core-api", "via": "slack:U1"}
)
# Crash before the resume worker ran; recover from disk only.
reopened = pipeline.reopen()
# Pre-sweep the record is still suspended (resume never ran).
assert reopened.load_record(suspended.thread_id).status is TaskStatus.WAITING_HUMAN
summary = reopened.startup_sweep(
questions_by_qid={suspended.question_id: ["which repo?"]}
)
assert summary["resumed"] == [suspended.thread_id]
assert reopened.load_record(suspended.thread_id).current_phase is Phase.PLAN
# ---------------------------------------------------------------------------
# (b) submit a duplicate answer and confirm it no-ops.
# ---------------------------------------------------------------------------
def test_b_duplicate_answer_no_ops(pipeline: SimPipeline) -> None:
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
raw = {
"question_id": suspended.question_id,
"answer": "core-api",
"via": "slack:U1",
}
first = pipeline.submit_answer(raw)
second = pipeline.submit_answer(raw) # exact redelivery / double click
third = pipeline.submit_answer(
{
"question_id": suspended.question_id,
"answer": "other-repo",
"via": "github:U2",
}
) # a different answer via a second channel
assert first is True
assert second is False
assert third is False
# The ledger preserved the *first* answer; later ones never overwrote it.
row = pipeline.ledger_row(suspended.question_id)
assert row["status"] == "answered"
assert json.loads(row["answer_json"]) == "core-api"
assert row["answered_via"] == "slack:U1"
def test_b_resume_is_single_apply_under_redelivered_resume(
pipeline: SimPipeline,
) -> None:
"""Even if the resume worker is invoked twice, it applies exactly once."""
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
assert pipeline.submit_answer(
{"question_id": suspended.question_id, "answer": "core-api", "via": "slack:U1"}
)
first = pipeline.resume(suspended.thread_id, suspended.question_id)
second = pipeline.resume(suspended.thread_id, suspended.question_id)
assert first.resumed is True
assert second.resumed is False
assert second.superseded is True # turn guard caught the stale resume
# The phase advanced exactly one step; the Q&A history has one entry.
record = pipeline.load_record(suspended.thread_id)
assert record.current_phase is Phase.PLAN
assert len(record.qa_history) == 1
assert pipeline.ledger_row(suspended.question_id)["status"] == "superseded"
# ---------------------------------------------------------------------------
# (c) answer after the deadline -> rejected, task parked.
# ---------------------------------------------------------------------------
def test_c_late_answer_rejected_and_task_parked(
pipeline: SimPipeline, clock: SimClock
) -> None:
suspended = pipeline.submit(questions=["which repo?"], deadline_in=50)
# Time passes beyond the deadline; the timer loop expires + parks.
clock.advance(51)
expired = pipeline.run_deadline_sweep()
assert expired == [suspended.question_id]
parked = pipeline.load_record(suspended.thread_id)
assert parked.status is TaskStatus.PARKED
assert parked.current_phase is Phase.PARKED
# A late answer loses the compare-and-set against the now-expired row.
won = pipeline.submit_answer(
{"question_id": suspended.question_id, "answer": "core-api", "via": "slack:U1"}
)
assert won is False
row = pipeline.ledger_row(suspended.question_id)
assert row["status"] == "expired"
assert row["answer_json"] is None
# A resume attempt on the expired question does nothing.
outcome = pipeline.resume(suspended.thread_id, suspended.question_id)
assert outcome.resumed is False
def test_c_deadline_vs_answer_race_answer_first_wins(
pipeline: SimPipeline, clock: SimClock
) -> None:
"""If the answer lands before the sweep, the sweep must not expire it."""
suspended = pipeline.submit(questions=["which repo?"], deadline_in=50)
assert pipeline.submit_answer(
{"question_id": suspended.question_id, "answer": "core-api", "via": "slack:U1"}
)
clock.advance(99) # well past the deadline
expired = pipeline.run_deadline_sweep()
# The question is already 'answered', so the sweep finds nothing to expire.
assert expired == []
assert pipeline.ledger_row(suspended.question_id)["status"] == "answered"
outcome = pipeline.resume(suspended.thread_id, suspended.question_id)
assert outcome.resumed is True
# ---------------------------------------------------------------------------
# (d) two tasks suspended concurrently resume independently to the correct thread.
# ---------------------------------------------------------------------------
def test_d_two_concurrent_tasks_resume_to_correct_thread(
pipeline: SimPipeline,
) -> None:
first = pipeline.submit(questions=["repo for A?"], deadline_in=100)
second = pipeline.submit(questions=["repo for B?"], deadline_in=100)
assert first.thread_id != second.thread_id
assert first.question_id != second.question_id
# Answer the second task first, with a distinct answer.
assert pipeline.submit_answer(
{"question_id": second.question_id, "answer": "repo-B", "via": "slack:U2"}
)
assert pipeline.submit_answer(
{"question_id": first.question_id, "answer": "repo-A", "via": "slack:U1"}
)
out_a = pipeline.resume(first.thread_id, first.question_id)
out_b = pipeline.resume(second.thread_id, second.question_id)
assert out_a.resumed and out_b.resumed
rec_a = pipeline.load_record(first.thread_id)
rec_b = pipeline.load_record(second.thread_id)
# Each thread carries *its own* answer — no cross-contamination.
assert rec_a.qa_history[0]["answer"] == "repo-A"
assert rec_b.qa_history[0]["answer"] == "repo-B"
assert rec_a.current_phase is Phase.PLAN
assert rec_b.current_phase is Phase.PLAN
def test_d_concurrent_responders_only_one_wins_per_question(
pipeline: SimPipeline,
) -> None:
"""Two threads racing the same question: exactly one compare-and-set wins.
Exercises the §3.3.1 ``BEGIN IMMEDIATE`` serialization in the committed
``answer_question`` helper under real OS threads against one SQLite file.
"""
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
barrier = threading.Barrier(2)
results: list[bool] = []
lock = threading.Lock()
def race(via: str) -> None:
barrier.wait()
try:
won = pipeline.submit_answer(
{"question_id": suspended.question_id, "answer": via, "via": via}
)
except (
sqlite3.OperationalError
): # pragma: no cover - lock contention is tolerated
won = False
with lock:
results.append(won)
threads = [threading.Thread(target=race, args=(f"slack:U{i}",)) for i in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert sum(1 for r in results if r) == 1 # exactly one winner
assert pipeline.ledger_row(suspended.question_id)["status"] == "answered"
def test_d_concurrent_tasks_survive_restart_independently(
pipeline: SimPipeline,
) -> None:
"""Two suspended tasks + a crash: each converges to its own thread."""
first = pipeline.submit(questions=["repo for A?"], deadline_in=100)
second = pipeline.submit(questions=["repo for B?"], deadline_in=100)
assert pipeline.submit_answer(
{"question_id": first.question_id, "answer": "repo-A", "via": "slack:U1"}
)
reopened = pipeline.reopen()
summary = reopened.startup_sweep(
questions_by_qid={
first.question_id: ["repo for A?"],
second.question_id: ["repo for B?"],
}
)
# Only the answered task resumes; the still-open one stays suspended.
assert summary["resumed"] == [first.thread_id]
assert reopened.load_record(first.thread_id).current_phase is Phase.PLAN
assert reopened.load_record(second.thread_id).status is TaskStatus.WAITING_HUMAN
# ---------------------------------------------------------------------------
# Supporting §3.3.1 edges: lost-post delivery + durable integrity.
# ---------------------------------------------------------------------------
def test_lost_post_leaves_open_row_then_reconcile_redelivers(
tmp_path_factory: pytest.TempPathFactory,
clock: SimClock,
post_failing_transport: PostFailingTransport,
) -> None:
pipeline = SimPipeline(
tmp_path_factory.mktemp("lostpost") / "state",
clock=clock,
transport=post_failing_transport,
)
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
# The post failed: the row is open with no channel_ref (no in-flight loss).
row = pipeline.ledger_row(suspended.question_id)
assert row["status"] == "open"
assert row["channel_ref"] is None
assert post_failing_transport.posts == []
# Reconcile retries idempotently; the second attempt succeeds.
redelivered = pipeline.reconcile(
questions_by_qid={suspended.question_id: ["which repo?"]}
)
assert redelivered == 1
row = pipeline.ledger_row(suspended.question_id)
assert row["channel_ref"] == f"sim:{suspended.question_id}"
def test_durable_checkpoint_is_integrity_checked(
pipeline: SimPipeline,
) -> None:
"""Corrupting the durable checkpoint must fail closed, not return junk."""
suspended = pipeline.submit(questions=["which repo?"], deadline_in=100)
checkpoint = pipeline._checkpoint_path(suspended.thread_id) # noqa: SLF001
# Tamper with the payload after the integrity sidecar was written.
checkpoint.write_bytes(checkpoint.read_bytes() + b"tampered")
with pytest.raises(IntegrityError):
pipeline.load_record(suspended.thread_id)