"""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)]