"""Approvals and feedback bind to the plan revision they were about (A4).""" import io from pathlib import Path from nisse_dev.canonical import UserMessage from nisse_dev.control import ( AWAIT_MILESTONE_SIGNOFF, Orchestrator, StubAgent, ) from nisse_dev.gitea import Comment from nisse_dev.oplog import OpLog from nisse_dev.state import StateStore from nisse_dev.transport import InMemoryTransport class Held: """A worker that queues jobs until released, like one under load.""" def __init__(self) -> None: self.jobs: list = [] self.busy = False def submit(self, job) -> None: self.jobs.append(job) def call(self, job, timeout=None): return job() def run(self) -> None: while self.jobs: self.jobs.pop(0)() class Board: def __init__(self) -> None: self.next_id = 100 def open_project_issue(self, project: str, task_id: str) -> int: return 10 def post_plan(self, issue: int, gate: str, rev: int, plan: str): self.next_id += 1 class Posted: comment_id = self.next_id return Posted() def echo(self, issue: int, text: str) -> None: pass def build(tmp_path: Path, worker: Held | None = None, board: Board | None = None): store = StateStore(tmp_path / "state.db", oplog=OpLog("nisse-dev", io.StringIO())) transport = InMemoryTransport() orch = Orchestrator( store, transport, StubAgent(), operators={"don"}, gitea_operators={"don"}, board=board, **({"worker": worker} if worker else {}), ) orch.start() return store, transport, orch def gate(store: StateStore) -> dict: return store.get_state().data.get("gate") or {} def test_revisions_run_across_the_task(tmp_path: Path) -> None: store, transport, _ = build(tmp_path) transport.receive("don", "use demo") assert gate(store)["rev"] == 1 transport.receive("don", "approve") assert gate(store) == {**gate(store), "name": "milestone-1", "rev": 2} assert "milestone-1 rev-2" in transport.outbound[-1].text def test_a_stale_approval_is_answered_and_a_fresh_one_works(tmp_path: Path) -> None: worker = Held() store, transport, _ = build(tmp_path, worker) transport.receive("don", "use demo") worker.run() transport.receive("don", "approve") transport.receive("don", "approve") # both while arch rev-1 was up worker.run() assert store.get_state().state == AWAIT_MILESTONE_SIGNOFF assert not gate(store)["approved"] assert "arrived while rev-1 was up" in transport.outbound[-1].text transport.receive("don", "approve") # now about milestone-1 rev-2 worker.run() assert gate(store)["approved"] def test_stale_feedback_is_recorded_but_rejects_nothing(tmp_path: Path) -> None: worker = Held() store, transport, _ = build(tmp_path, worker) transport.receive("don", "use demo") worker.run() transport.receive("don", "approve") transport.receive("don", "also, keep it in one file") worker.run() assert store.get_state().state == AWAIT_MILESTONE_SIGNOFF assert gate(store)["rev"] == 2 assert "not taken as feedback" in transport.outbound[-1].text said = [ e.event.text for e in store.read_events() if isinstance(e.event, UserMessage) ] assert "also, keep it in one file" in said # the planner still hears it def test_a_gitea_reply_binds_to_the_latest_plan_posted_before_it( tmp_path: Path, ) -> None: board = Board() store, transport, orch = build(tmp_path, board=board) transport.receive("don", "use demo") # arch rev-1 posted as comment 101 orch._handle_plan_comment_now( Comment(id=102, author="don", body="approve", created_at="t") ) # Approved arch rev-1; milestone-1 rev-2 is now posted as comment 103. assert gate(store) == {**gate(store), "name": "milestone-1", "approved": False} # A reply older than that post was about arch rev-1: not applied. orch._handle_plan_comment_now( Comment(id=102, author="don", body="approve", created_at="t") ) assert not gate(store)["approved"] # One after it is about milestone-1 rev-2. orch._handle_plan_comment_now( Comment(id=104, author="don", body="approve", created_at="t") ) assert gate(store)["approved"] def test_the_plan_up_survives_a_restart(tmp_path: Path) -> None: _, transport, _ = build(tmp_path) transport.receive("don", "use demo") # arch rev-1 up; no board, so unsaved store, transport, _ = build(tmp_path) # restart at the open gate transport.receive("don", "approve") # stamped with the restored rev-1 assert store.get_state().state == AWAIT_MILESTONE_SIGNOFF assert gate(store)["name"] == "milestone-1" assert gate(store)["rev"] == 2