atlas-iac/testing/tests/test_hermes_publication_retry_fence.py

122 lines
5.1 KiB
Python

"""Transactional publication-retry fence coverage."""
from __future__ import annotations
import json
import sqlite3
from contextlib import contextmanager
from testing.tests.test_hermes_cli_support import _load
fence = _load("publication_retry_fence")
class Native:
"""Minimal existing-native transaction surface used by the adapter."""
@staticmethod
@contextmanager
def write_txn(connection):
connection.execute("BEGIN IMMEDIATE")
try:
yield
except Exception:
connection.rollback()
raise
else:
connection.commit()
@staticmethod
def _append_event(connection, task_id, kind, payload):
connection.execute(
"INSERT INTO task_events(task_id,run_id,kind,payload) VALUES(?,?,?,?)",
(task_id, None, kind, json.dumps(payload, sort_keys=True)),
)
def _database():
connection = sqlite3.connect(":memory:")
connection.row_factory = sqlite3.Row
connection.executescript("""
CREATE TABLE tasks(id TEXT PRIMARY KEY,status TEXT,current_run_id INTEGER,
consecutive_failures INTEGER,last_failure_error TEXT);
CREATE TABLE task_links(child_id TEXT,parent_id TEXT);
CREATE TABLE task_events(id INTEGER PRIMARY KEY AUTOINCREMENT,task_id TEXT,
run_id INTEGER,kind TEXT,payload TEXT);
""")
connection.execute("INSERT INTO tasks VALUES('root','done',NULL,0,NULL)")
connection.execute("INSERT INTO tasks VALUES('child','blocked',NULL,1,'broker')")
connection.execute("INSERT INTO task_links VALUES('child','root')")
return connection
def _blocked(connection, marker):
event_id = connection.execute(
"INSERT INTO task_events(task_id,run_id,kind,payload) VALUES(?,?,?,?)",
("child", 8, "blocked", json.dumps({"reason": "transport\\n" + marker})),
).lastrowid
connection.commit()
return event_id
def test_adapter_reopens_only_the_exact_native_fence():
"""The existing native transaction protects the event check and state change."""
connection = _database()
marker = "[hermes-publication-retry-fence:8:" + "a" * 64 + "]"
event_id = _blocked(connection, marker)
assert fence.guarded_unblock(Native, connection, "child", "root", "8", (event_id, marker))
assert connection.execute("SELECT status FROM tasks WHERE id='child'").fetchone()[0] == "ready"
assert connection.execute("SELECT kind FROM task_events ORDER BY id DESC LIMIT 1").fetchone()[0] == "unblocked"
def test_adapter_keeps_a_later_human_transient_block_closed():
"""A later event, even with the same block kind, invalidates coordinator ownership."""
connection = _database()
marker = "[hermes-publication-retry-fence:8:" + "b" * 64 + "]"
event_id = _blocked(connection, marker)
connection.execute(
"INSERT INTO task_events(task_id,run_id,kind,payload) VALUES(?,?,?,?)",
("child", None, "blocked", json.dumps({"reason": "human decision"})),
)
connection.commit()
assert not fence.guarded_unblock(Native, connection, "child", "root", "8", (event_id, marker))
assert connection.execute("SELECT status FROM tasks WHERE id='child'").fetchone()[0] == "blocked"
def test_block_records_the_exact_native_event_as_coordinator_owned(tmp_path, monkeypatch):
"""Only the coordinator's successful fenced block creates a sidecar ownership row."""
monkeypatch.setattr(fence.supervisor_state, "KANBAN_ROOT", tmp_path / "boards")
connection = _database()
connection.execute("UPDATE tasks SET status='running',current_run_id=8 WHERE id='child'")
connection.commit()
class BlockingNative(Native):
@staticmethod
def block_task(conn, task_id, *, reason, kind, expected_run_id):
changed = conn.execute(
"UPDATE tasks SET status='blocked',current_run_id=NULL WHERE id=? AND status='running' AND current_run_id=?",
(task_id, expected_run_id),
).rowcount
if changed:
conn.execute(
"INSERT INTO task_events(task_id,run_id,kind,payload) VALUES(?,?,?,?)",
(task_id, expected_run_id, "blocked", json.dumps({"reason": reason, "kind": kind})),
)
return bool(changed)
receipt = {"result_digest": "c" * 64}
assert fence.block(BlockingNative, connection, "soteria", "child", "8", "transport", "transient", receipt)
event_id = connection.execute("SELECT id FROM task_events ORDER BY id DESC LIMIT 1").fetchone()[0]
marker = "[hermes-publication-retry-fence:8:" + "c" * 64 + "]"
assert fence.read("soteria", "child", "8") == (event_id, marker)
# Simulate a crash after native block but before its sidecar fence record.
with fence.supervisor_state._connect("soteria") as state:
state.execute("DELETE FROM publication_retry_fences")
state.execute(
"INSERT INTO publication_retries(board,child_task_id,source_run_id,source_ordinal,receipt_json) VALUES(?,?,?,?,?)",
("soteria", "child", "8", 0, json.dumps(receipt)),
)
assert fence.recover("soteria", "child", "8", connection, receipt) == (event_id, marker)