"""Native-shaped regression tests for coordinator-owned PR continuations.""" from __future__ import annotations from contextlib import nullcontext import json from pathlib import Path import sys import pytest from testing.tests.test_hermes_cli_support import HERMES, _load sys.path.insert(0, str(HERMES / "scm-common/scripts")) state = _load("supervisor_state") continuation = _load("kanban_continue_pr") class NativeKanban: """Match live ``kanban_db``'s keyword-only create API without metadata.""" def __init__(self) -> None: self.created: list[dict] = [] self.by_key: dict[str, str] = {} self.comments: list[tuple] = [] def scoped_current_board(self, _board: str): return nullcontext() def connect(self, *, board: str): class Connection: def close(self): return None return Connection() def get_task(self, _conn, task_id: str): return {"id": task_id} if task_id == "root" else None def create_task(self, _conn, *, title, body=None, assignee=None, created_by=None, workspace_kind="scratch", workspace_path=None, branch_name=None, tenant=None, priority=0, parents=(), triage=False, idempotency_key=None, max_runtime_seconds=None, skills=None, max_retries=None, goal_mode=False, goal_max_turns=None, initial_status="running", session_id=None, board=None, project_id=None): assert branch_name is None assert workspace_kind == "scratch" assert idempotency_key if idempotency_key in self.by_key: return self.by_key[idempotency_key] task_id = f"child-{len(self.created) + 1}" self.by_key[idempotency_key] = task_id self.created.append({ "id": task_id, "title": title, "body": body, "assignee": assignee, "created_by": created_by, "parents": tuple(parents), "idempotency_key": idempotency_key, "initial_status": initial_status, }) return task_id def add_comment(self, _conn, *args): self.comments.append(args) @pytest.fixture def trusted_root(tmp_path: Path, monkeypatch): """Make a real SQLite board DB holding a signed root lineage record.""" board = "titan-iac" monkeypatch.setattr(state, "KANBAN_ROOT", tmp_path / "boards") path = state.KANBAN_ROOT / board / "kanban.db" lineage = state.Lineage( root_task_id="root", project="atlas-iac", branch="hermes/root", pull_request="https://scm.bstein.dev/titan/atlas-iac/pulls/7", base_branch="main", ) state.record_submission(board, "root", lineage, "a" * 40, path=path) state.set_ready(board, "root", "a" * 40, path=path) return board, lineage, path def _pr(head: str) -> bytes: return json.dumps({ "state": "open", "head": {"ref": "hermes/root", "sha": head, "repo": {"full_name": "titan/atlas-iac"}}, "base": {"ref": "main", "repo": {"full_name": "titan/atlas-iac"}}, }).encode() def test_queue_uses_real_sqlite_lineage_and_native_create_signature(trusted_root, monkeypatch): board, _lineage, path = trusted_root native = NativeKanban() monkeypatch.setattr(continuation.scm_broker_client, "read", lambda _path: _pr("b" * 40)) child, created = continuation.queue(native, board=board, root_task="root", objective="fix review") assert (child, created) == ("child-1", True) assert native.created[0]["parents"] == ("root",) assert native.created[0]["initial_status"] == "running" assert "metadata" not in native.created[0] assert state.get_live_head(board, "root", path=path) == "b" * 40 assert state.get_child(board, child, path=path)["parent_task_id"] == "root" with state._connect(board, path) as connection: ready = connection.execute( "SELECT ready_for_human_merge,ready_commit FROM supervisor_roots" ).fetchone() assert tuple(ready) == (0, "") same, created = continuation.queue(native, board=board, root_task="root", objective="fix review") assert (same, created) == (child, False) assert len(native.created) == 1 def test_interrupted_recording_recovers_through_native_idempotency(trusted_root, monkeypatch): board, _lineage, path = trusted_root native = NativeKanban() monkeypatch.setattr(continuation.scm_broker_client, "read", lambda _path: _pr("a" * 40)) original = continuation.supervisor_state.record_child monkeypatch.setattr( continuation.supervisor_state, "record_child", lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("simulated restart")), ) with pytest.raises(OSError, match="simulated restart"): continuation.queue(native, board=board, root_task="root", objective="repair flaky test") assert len(native.created) == 1 monkeypatch.setattr(continuation.supervisor_state, "record_child", original) child, created = continuation.queue(native, board=board, root_task="root", objective="repair flaky test") assert (child, created) == ("child-1", True) assert len(native.created) == 1 assert state.get_child(board, child, path=path) is not None def test_submission_cannot_rewrite_root_and_new_head_clears_ready(trusted_root): board, lineage, path = trusted_root replacement = state.Lineage( root_task_id="root", project="atlas-iac", branch="attacker/ref", pull_request=lineage.pull_request, base_branch="main", ) with pytest.raises(ValueError, match="immutable"): state.record_submission(board, "root", replacement, "c" * 40, path=path) state.record_submission(board, "repair-child", lineage, "c" * 40, path=path) with state._connect(board, path) as connection: row = connection.execute( "SELECT live_pr_head,ready_for_human_merge,ready_commit FROM supervisor_roots" ).fetchone() assert tuple(row) == ("c" * 40, 0, "") def test_present_malformed_continuation_state_fails_closed(trusted_root): board, _lineage, path = trusted_root state.record_child(board, "child", "root", "root", "repair", "a" * 40, "fix", path=path) with state._connect(board, path) as connection: connection.execute( "UPDATE supervisor_children SET cycle=? WHERE board=? AND child_task_id=?", ("not-a-cycle", board, "child"), ) with pytest.raises(state.SupervisorStateError, match="malformed"): state.get_child(board, "child", path=path)