atlas-iac/testing/tests/test_hermes_kanban_continuation.py

237 lines
10 KiB
Python

"""Native-shaped regression tests for coordinator-owned PR continuations."""
from __future__ import annotations
from contextlib import nullcontext
import json
from pathlib import Path
import sqlite3
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)
assert state.get_root(board, "root") == lineage
return board, lineage, state.KANBAN_ROOT / board / state.STATE_FILE
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)
def test_default_state_sidecar_imports_healthy_legacy_rows_once(tmp_path, monkeypatch):
"""The first isolated state DB preserves native rows without sharing its file."""
board = "titan-iac"
monkeypatch.setattr(state, "KANBAN_ROOT", tmp_path / "boards")
legacy = state.KANBAN_ROOT / board / "kanban.db"
lineage = state.Lineage("root", "feature/root", "https://scm.bstein.dev/titan/atlas-iac/pulls/9", "atlas-iac", "main")
state.record_submission(board, "root", lineage, "a" * 40, path=legacy)
state.set_ready(board, "root", "a" * 40, path=legacy)
state.record_child(board, "repair", "root", "root", "repair", "a" * 40, "repair", path=legacy)
assert state.get_root(board, "root") == lineage
assert state.get_child(board, "repair")["parent_task_id"] == "root"
sidecar = state.KANBAN_ROOT / board / state.STATE_FILE
assert sidecar.is_file() and sidecar != legacy
state.record_submission(board, "root", lineage, "b" * 40)
assert state.get_live_head(board, "root") == "b" * 40
assert state.get_live_head(board, "root", path=legacy) == "a" * 40
def test_default_state_refuses_corrupt_legacy_without_empty_sidecar(tmp_path, monkeypatch):
"""A damaged native DB cannot silently erase supervisor authority."""
board = "soteria"
monkeypatch.setattr(state, "KANBAN_ROOT", tmp_path / "boards")
legacy = state.KANBAN_ROOT / board / "kanban.db"
legacy.parent.mkdir(parents=True)
legacy.write_bytes(b"not a sqlite database")
with pytest.raises(state.SupervisorStateError, match="unreadable"):
state.get_root(board, "root")
assert not (state.KANBAN_ROOT / board / state.STATE_FILE).exists()
def test_default_state_imports_additive_legacy_schema_and_rejects_orphans(tmp_path, monkeypatch):
"""Older valid rows keep their safe defaults; orphan children stop migration."""
board = "atlas-iac"
monkeypatch.setattr(state, "KANBAN_ROOT", tmp_path / "boards")
legacy = state.KANBAN_ROOT / board / "kanban.db"
legacy.parent.mkdir(parents=True)
schema = """
CREATE TABLE supervisor_roots (
board TEXT, root_task_id TEXT, project TEXT, branch TEXT, pull_request TEXT,
base_branch TEXT, live_pr_head TEXT
);
CREATE TABLE supervisor_children (
board TEXT, child_task_id TEXT, root_task_id TEXT, parent_task_id TEXT,
kind TEXT, head_commit TEXT, objective_digest TEXT
);
"""
with sqlite3.connect(legacy) as connection:
connection.executescript(schema)
connection.execute(
"INSERT INTO supervisor_roots VALUES(?,?,?,?,?,?,?)",
(board, "root", "atlas-iac", "feature/root", "pull/1", "main", "a" * 40),
)
connection.execute(
"INSERT INTO supervisor_children VALUES(?,?,?,?,?,?,?)",
(board, "child", "root", "root", "repair", "a" * 40, "digest"),
)
assert state.get_child(board, "child")["cycle"] == 1
with state._connect(board) as connection:
assert connection.execute("SELECT ready_for_human_merge,ready_commit FROM supervisor_roots").fetchone() == (0, "")
orphan_board = "soteria"
orphan = state.KANBAN_ROOT / orphan_board / "kanban.db"
orphan.parent.mkdir()
with sqlite3.connect(orphan) as connection:
connection.executescript(schema)
connection.execute(
"INSERT INTO supervisor_children VALUES(?,?,?,?,?,?,?)",
(orphan_board, "child", "missing", "missing", "repair", "a" * 40, "digest"),
)
with pytest.raises(state.SupervisorStateError, match="malformed"):
state.get_root(orphan_board, "missing")
assert not (state.KANBAN_ROOT / orphan_board / state.STATE_FILE).exists()