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>
266 lines
8.9 KiB
Python
266 lines
8.9 KiB
Python
"""Adversarial branch coverage for CLI lane foundation helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import runpy
|
|
import sqlite3
|
|
from contextlib import nullcontext
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from urllib.error import URLError
|
|
|
|
import pytest
|
|
|
|
from testing.tests.test_hermes_cli_support import SCRIPTS, _completed_result, lanes
|
|
|
|
|
|
def test_board_context_serializes_objects_and_storage_failure_is_bounded(
|
|
monkeypatch,
|
|
):
|
|
db = SimpleNamespace(build_worker_context=lambda _conn, _task: {"priority": 3})
|
|
assert lanes._task_context(db, object(), "t_ctx") == '{\n "priority": 3\n}'
|
|
|
|
connections = []
|
|
|
|
class Connection:
|
|
def close(self):
|
|
connections.append("closed")
|
|
|
|
failing = SimpleNamespace(
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
connect=lambda board: Connection(),
|
|
)
|
|
monkeypatch.setattr(lanes, "KANBAN_STORAGE_ATTEMPTS", 2)
|
|
monkeypatch.setattr(lanes.time, "sleep", lambda _seconds: None)
|
|
with pytest.raises(sqlite3.OperationalError, match="volume unavailable"):
|
|
lanes._board_call(
|
|
failing,
|
|
"cassandra",
|
|
lambda _conn: (_ for _ in ()).throw(
|
|
sqlite3.OperationalError("volume unavailable")
|
|
),
|
|
)
|
|
assert connections == ["closed", "closed"]
|
|
|
|
|
|
def test_atomic_json_closes_an_unwrapped_descriptor_on_open_failure(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
):
|
|
destination = tmp_path / "state.json"
|
|
real_fdopen = lanes.os.fdopen
|
|
descriptors = []
|
|
|
|
def fail_fdopen(descriptor, *_args, **_kwargs):
|
|
descriptors.append(descriptor)
|
|
raise OSError("cannot wrap descriptor")
|
|
|
|
monkeypatch.setattr(lanes.os, "fdopen", fail_fdopen)
|
|
with pytest.raises(OSError, match="cannot wrap"):
|
|
lanes.atomic_json(destination, {"safe": True})
|
|
monkeypatch.setattr(lanes.os, "fdopen", real_fdopen)
|
|
|
|
assert descriptors
|
|
with pytest.raises(OSError):
|
|
lanes.os.fstat(descriptors[0])
|
|
assert list(tmp_path.glob("*.tmp")) == []
|
|
|
|
|
|
def test_json_and_terminal_path_helpers_fail_closed(tmp_path: Path, monkeypatch):
|
|
invalid = tmp_path / "invalid.json"
|
|
invalid.write_bytes(b"\xff")
|
|
assert lanes.load_json(invalid) == {}
|
|
array = tmp_path / "array.json"
|
|
array.write_text("[]", encoding="utf-8")
|
|
assert lanes.load_json(array) == {}
|
|
|
|
with pytest.raises(ValueError, match="journal state"):
|
|
lanes._terminal_path(tmp_path / "task.json", 1, "prepared")
|
|
with pytest.raises(ValueError, match="SQLite"):
|
|
lanes._terminal_path(tmp_path / "task.json", True)
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "lanes")
|
|
outside = tmp_path / "outside.run-1.terminal.pending.json"
|
|
assert lanes._terminal_identity(outside) is None
|
|
nested = lanes.STATE_ROOT / "board" / "nested" / "x.json"
|
|
assert lanes._terminal_identity(nested) is None
|
|
missing = lanes.STATE_ROOT / "board" / "t.run-1.terminal.pending.json"
|
|
assert lanes._terminal_identity(missing) is None
|
|
assert lanes._terminal_evidence_identity(outside) is None
|
|
assert lanes._terminal_evidence_identity(nested) is None
|
|
missing_evidence = lanes.STATE_ROOT / "board" / (
|
|
"t.run-1.terminal.prepared-" + "a" * 32 + ".json"
|
|
)
|
|
assert lanes._terminal_evidence_identity(missing_evidence) is None
|
|
|
|
with pytest.raises(ValueError, match="positive SQLite"):
|
|
lanes.TerminalIdentity("board", "task", 0, "pending")
|
|
|
|
|
|
def test_recovery_snapshot_close_is_idempotent_after_descriptors_are_released(
|
|
tmp_path: Path,
|
|
):
|
|
directory = os.open(tmp_path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
|
snapshot = lanes.TerminalRecoverySnapshot(
|
|
None,
|
|
tmp_path.stat(),
|
|
None,
|
|
directory,
|
|
b"",
|
|
"synthetic",
|
|
)
|
|
snapshot.close()
|
|
snapshot.close()
|
|
assert snapshot.directory_descriptor == -1
|
|
|
|
|
|
def test_goal_helpers_cover_compaction_and_deterministic_failures():
|
|
compacted = lanes.cli_lane_goal._bounded("a" * 100, 40)
|
|
assert len(compacted) == 40
|
|
assert "compacted" in compacted
|
|
assert lanes.cli_lane_goal.unfinished_result_reason(
|
|
{"status": "incomplete", "summary": ""}
|
|
) == "worker explicitly reported incomplete work"
|
|
assert lanes.cli_lane_goal.unfinished_result_reason(
|
|
{"status": "blocked", "summary": "waiting"}
|
|
) is None
|
|
assert "blockers" in lanes.cli_lane_goal.unfinished_result_reason(
|
|
{"status": "completed", "summary": "done", "blockers": ["remote"]}
|
|
)
|
|
assert "unfinished" in lanes.cli_lane_goal.unfinished_result_reason(
|
|
{
|
|
"status": "completed",
|
|
"summary": "verification is pending",
|
|
"blockers": [],
|
|
"tests_run": [],
|
|
}
|
|
)
|
|
accepted, reason = lanes.cli_lane_goal.judge_goal_completion(
|
|
"finish",
|
|
{"status": "incomplete", "summary": "still running"},
|
|
open_request=lambda *_args, **_kwargs: pytest.fail("network must not run"),
|
|
)
|
|
assert accepted is False
|
|
assert reason
|
|
assert lanes.cli_lane_goal.unfinished_result_reason(
|
|
{"status": "completed", "summary": "done", "tests_run": "not-a-list"}
|
|
) is None
|
|
|
|
|
|
def test_prompt_artifact_and_json_helpers_reject_unsafe_inputs(
|
|
tmp_path: Path,
|
|
):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
artifact = workspace / "evidence.txt"
|
|
artifact.write_text("evidence", encoding="utf-8")
|
|
outside = tmp_path / "outside.txt"
|
|
outside.write_text("outside", encoding="utf-8")
|
|
directory = workspace / "directory"
|
|
directory.mkdir()
|
|
|
|
assert lanes.workspace_artifacts(workspace, "not-a-list") == []
|
|
assert lanes.workspace_artifacts(
|
|
workspace,
|
|
[None, "", "missing", outside, directory, artifact, str(artifact)],
|
|
) == [str(artifact.resolve())]
|
|
assert lanes._extract_json({"status": "completed"}) == {"status": "completed"}
|
|
assert lanes._extract_json(42) is None
|
|
assert lanes._extract_json("not json") is None
|
|
assert lanes._extract_json('prefix {"status":"blocked"} suffix') == {
|
|
"status": "blocked"
|
|
}
|
|
assert lanes._extract_json('{"status":"unknown"}') is None
|
|
|
|
state_file = workspace / "state.json"
|
|
state = {}
|
|
assert lanes._event_payload(
|
|
"codex",
|
|
json.dumps({"type": "thread.started", "result": {"status": "blocked"}}),
|
|
state,
|
|
state_file,
|
|
) == {"status": "blocked"}
|
|
assert lanes._event_payload(
|
|
"claude",
|
|
json.dumps({"session_id": "session-2", "text": "not-json"}),
|
|
state,
|
|
state_file,
|
|
) is None
|
|
assert state["claude_session_id"] == "session-2"
|
|
|
|
prompt_module = __import__("cli_lane_prompt")
|
|
assert prompt_module.workspace_artifacts(
|
|
workspace, [str(directory), str(artifact)]
|
|
) == [str(artifact.resolve())]
|
|
assert prompt_module._event_payload(
|
|
"other", '{"item": {}}', {}, state_file
|
|
) is None
|
|
|
|
|
|
def test_provider_event_parsing_persists_sessions_and_nested_results(
|
|
tmp_path: Path,
|
|
):
|
|
state_file = tmp_path / "state.json"
|
|
state = {}
|
|
assert lanes._event_payload("codex", "not-json", state, state_file) is None
|
|
assert lanes._event_payload("codex", "[]", state, state_file) is None
|
|
assert (
|
|
lanes._event_payload(
|
|
"codex",
|
|
json.dumps({"type": "thread.started", "thread": {"id": "thread-1"}}),
|
|
state,
|
|
state_file,
|
|
)
|
|
is None
|
|
)
|
|
assert state["codex_thread_id"] == "thread-1"
|
|
result = lanes._event_payload(
|
|
"claude",
|
|
json.dumps(
|
|
{
|
|
"session_id": "session-1",
|
|
"item": {"content": json.dumps(_completed_result("nested"))},
|
|
}
|
|
),
|
|
state,
|
|
state_file,
|
|
)
|
|
assert result and result["summary"] == "nested"
|
|
assert state["claude_session_id"] == "session-1"
|
|
|
|
|
|
@pytest.mark.parametrize("assignee", ["cli-codex-max", "worker-auto", ""])
|
|
def test_routing_rejects_invalid_assignees(assignee: str):
|
|
with pytest.raises(ValueError, match="unsupported"):
|
|
lanes.parse_assignee(assignee)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target",
|
|
["codex/model/high", "worker/local/model/high", "worker/codex/model/max"],
|
|
)
|
|
def test_worker_target_decoder_rejects_invalid_targets(target: str):
|
|
with pytest.raises(RuntimeError):
|
|
lanes._decode_worker_target(target)
|
|
|
|
|
|
def test_switchyard_network_failure_is_explicit():
|
|
with pytest.raises(RuntimeError, match="routing failed"):
|
|
lanes.select_route(
|
|
"objective",
|
|
"cli-auto",
|
|
open_request=lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
URLError("offline")
|
|
),
|
|
)
|
|
|
|
|
|
def test_runner_main_guard_delegates_to_dispatch(monkeypatch):
|
|
dispatch = __import__("cli_lane_dispatch")
|
|
monkeypatch.setattr(dispatch, "main", lambda: 17)
|
|
with pytest.raises(SystemExit) as raised:
|
|
runpy.run_path(str(SCRIPTS / "cli_lane_runner.py"), run_name="__main__")
|
|
assert raised.value.code == 17
|