Three fenced worker Pods claim Hermes Kanban runs through a coordinator that owns every state transition, with per-ordinal HMAC authority, a mediated broker-only SCM path, and durable per-ordinal workspaces. Content is the reviewed head of PR #18 (689bcb6e) with PR 16's and PR 19's contributions removed: they were merged in only to validate co-existence and are not prerequisites, so this branch no longer carries them as ancestors. Only PR 14 and PR 15 remain, because the broker boundary and the cli_lane_* decomposition are load-bearing for two of the fixed P0 boundaries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
277 lines
8.1 KiB
Python
277 lines
8.1 KiB
Python
"""Mixed-image capability gates for mounted Hermes CLI lane scripts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from contextlib import nullcontext
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from testing.tests.test_hermes_cli_support import _completed_result, lanes
|
|
|
|
|
|
def _old_complete(
|
|
_conn,
|
|
_task_id,
|
|
*,
|
|
result=None,
|
|
summary=None,
|
|
metadata=None,
|
|
expected_run_id=None,
|
|
):
|
|
return bool(result or summary or metadata or expected_run_id)
|
|
|
|
|
|
def _new_complete(
|
|
_conn,
|
|
_task_id,
|
|
*,
|
|
result=None,
|
|
summary=None,
|
|
metadata=None,
|
|
expected_run_id=None,
|
|
replay_ended_run_id=None,
|
|
):
|
|
return bool(result or summary or metadata or expected_run_id or replay_ended_run_id)
|
|
|
|
|
|
def _old_reclaim(_conn, _task_id, *, reason, signal_fn=None):
|
|
return bool(reason or signal_fn)
|
|
|
|
|
|
def _new_reclaim(
|
|
_conn,
|
|
_task_id,
|
|
*,
|
|
reason,
|
|
signal_fn=None,
|
|
expected_run_id=None,
|
|
):
|
|
return bool(reason or signal_fn or expected_run_id)
|
|
|
|
|
|
def _db(*, patched: bool):
|
|
return SimpleNamespace(
|
|
complete_task=_new_complete if patched else _old_complete,
|
|
reclaim_task=_new_reclaim if patched else _old_reclaim,
|
|
)
|
|
|
|
|
|
def _terminal_document(run_id: int = 7) -> dict:
|
|
structured = _completed_result("accepted result")
|
|
return {
|
|
"board": "cassandra",
|
|
"task_id": "t_compat",
|
|
"expected_run_id": run_id,
|
|
"result": json.dumps(structured, sort_keys=True),
|
|
"summary": "accepted result",
|
|
"metadata": {},
|
|
"kanban_state": "pending",
|
|
"recorded_at": lanes.utc_now(),
|
|
}
|
|
|
|
|
|
def test_capability_detection_requires_explicit_safety_keywords():
|
|
old = lanes.detect_kanban_capabilities(_db(patched=False))
|
|
new = lanes.detect_kanban_capabilities(_db(patched=True))
|
|
|
|
assert old.exact_run_completion is True
|
|
assert old.ended_run_replay is False
|
|
assert old.exact_run_reclaim is False
|
|
assert old.ready is False
|
|
assert old.deferred_features == ("ended-run-replay", "exact-run-reclaim")
|
|
assert new.ready is True
|
|
assert new.deferred_features == ()
|
|
|
|
variadic = SimpleNamespace(
|
|
complete_task=lambda *_args, **_kwargs: True,
|
|
reclaim_task=lambda *_args, **_kwargs: True,
|
|
)
|
|
assert lanes.detect_kanban_capabilities(variadic).ready is False
|
|
assert lanes._explicit_keyword(object(), "expected_run_id") is False
|
|
|
|
missing_completion = lanes.KanbanCapabilities(False, True, True)
|
|
assert missing_completion.deferred_features == ("exact-run-completion",)
|
|
|
|
|
|
def test_startup_health_moves_from_bounded_deferred_to_ready(
|
|
tmp_path: Path,
|
|
capsys,
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
|
|
health = tmp_path / "runtime-health.json"
|
|
old = lanes.initialize_kanban_capabilities(_db(patched=False), health_path=health)
|
|
old_document = lanes.runtime_health(health)
|
|
|
|
assert old.ready is False
|
|
assert old_document["state"] == "deferred"
|
|
assert old_document["ready"] is False
|
|
assert old_document["active_run_completion_safe"] is True
|
|
assert old_document["deferred_features"] == [
|
|
"ended-run-replay",
|
|
"exact-run-reclaim",
|
|
]
|
|
assert health.stat().st_size < 2048
|
|
assert health.stat().st_mode & 0o777 == 0o600
|
|
assert "compatibility deferred" in capsys.readouterr().err
|
|
|
|
new_db = _db(patched=True)
|
|
lanes.kanban_capabilities(new_db)
|
|
new = lanes.initialize_kanban_capabilities(new_db, health_path=health)
|
|
assert new.ready is True
|
|
assert lanes.kanban_capabilities(new_db) is new
|
|
assert lanes.runtime_health(health)["state"] == "ready"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("status", "run_id", "expected"),
|
|
[("running", 7, "committed"), ("blocked", None, "deferred")],
|
|
)
|
|
def test_old_image_completes_only_the_exact_active_run(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
status: str,
|
|
run_id: int | None,
|
|
expected: str,
|
|
):
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
|
|
task = SimpleNamespace(
|
|
id="t_compat",
|
|
status=status,
|
|
current_run_id=run_id,
|
|
completed_run_id=None,
|
|
result=None,
|
|
assignee="cli-auto",
|
|
)
|
|
calls = []
|
|
|
|
def complete(
|
|
_conn,
|
|
_task_id,
|
|
*,
|
|
result=None,
|
|
summary=None,
|
|
metadata=None,
|
|
expected_run_id=None,
|
|
):
|
|
calls.append(expected_run_id)
|
|
task.status = "done"
|
|
task.current_run_id = None
|
|
task.completed_run_id = expected_run_id
|
|
task.result = result
|
|
return True
|
|
|
|
db = SimpleNamespace(
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
connect=lambda board: SimpleNamespace(close=lambda: None),
|
|
get_task=lambda _conn, _task_id: task,
|
|
complete_task=complete,
|
|
reclaim_task=_old_reclaim,
|
|
)
|
|
identity = lanes.TerminalIdentity("cassandra", "t_compat", 7, "pending")
|
|
|
|
outcome = lanes._finalize_document_db(db, identity, _terminal_document())
|
|
|
|
assert outcome == expected
|
|
assert calls == ([7] if status == "running" else [])
|
|
if expected == "deferred":
|
|
assert task.status == "blocked"
|
|
|
|
|
|
def test_old_image_preserves_pending_and_prepared_journals(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
|
|
task = SimpleNamespace(
|
|
id="t_compat",
|
|
status="blocked",
|
|
current_run_id=None,
|
|
completed_run_id=None,
|
|
result=None,
|
|
assignee="cli-auto",
|
|
)
|
|
db = SimpleNamespace(
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
connect=lambda board: SimpleNamespace(close=lambda: None),
|
|
get_task=lambda _conn, _task_id: task,
|
|
complete_task=_old_complete,
|
|
reclaim_task=_old_reclaim,
|
|
)
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
pending, _record = lanes._write_terminal_record(
|
|
lanes.state_path("cassandra", "t_compat"),
|
|
board="cassandra",
|
|
task_id="t_compat",
|
|
run_id=7,
|
|
structured=_completed_result("accepted result"),
|
|
summary="accepted result",
|
|
metadata={},
|
|
)
|
|
|
|
assert lanes.recover_pending_finalizations() == 0
|
|
assert pending.exists()
|
|
assert len(list(pending.parent.glob("*.terminal.prepared-*.json"))) == 1
|
|
assert task.status == "blocked"
|
|
|
|
|
|
def test_old_image_never_calls_unguarded_reclaim(tmp_path: Path, monkeypatch):
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
|
|
task = SimpleNamespace(
|
|
id="t_running",
|
|
status="running",
|
|
current_run_id=11,
|
|
assignee="cli-auto",
|
|
)
|
|
calls = []
|
|
|
|
def reclaim(_conn, _task_id, *, reason, signal_fn=None):
|
|
calls.append((reason, signal_fn))
|
|
return True
|
|
|
|
db = SimpleNamespace(
|
|
complete_task=_old_complete,
|
|
reclaim_task=reclaim,
|
|
list_boards=lambda include_archived=False: [{"slug": "cassandra"}],
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
connect=lambda board: SimpleNamespace(close=lambda: None),
|
|
list_tasks=lambda _conn: [task],
|
|
)
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
monkeypatch.setattr(lanes, "recover_pending_finalizations", lambda: 0)
|
|
|
|
lanes.recover_orphans()
|
|
|
|
assert calls == []
|
|
|
|
|
|
def test_main_detects_capabilities_before_startup_recovery(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
):
|
|
events = []
|
|
db = _db(patched=False)
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
monkeypatch.setattr(lanes, "RESULT_SCHEMA_PATH", tmp_path / "result.schema.json")
|
|
monkeypatch.setattr(
|
|
lanes,
|
|
"initialize_kanban_capabilities",
|
|
lambda candidate: events.append(("detect", candidate))
|
|
or lanes.KanbanCapabilities(True, False, False),
|
|
)
|
|
|
|
def stop_after_detection():
|
|
events.append(("recover", db))
|
|
raise RuntimeError("stop after startup boundary")
|
|
|
|
monkeypatch.setattr(lanes, "recover_orphans", stop_after_detection)
|
|
|
|
with pytest.raises(RuntimeError, match="startup boundary"):
|
|
lanes.main()
|
|
|
|
assert events == [("detect", db), ("recover", db)]
|