2026-08-17 08:16:35 -03:00
|
|
|
"""Execution-loop edge coverage for preparation, callbacks, and recovery."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
import sys
|
|
|
|
|
from contextlib import nullcontext
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
|
|
|
|
from testing.tests.test_hermes_cli_support import _completed_result, lanes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Connection:
|
|
|
|
|
def close(self):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2026-09-13 15:04:01 -05:00
|
|
|
def test_quality_retry_uses_sol_xhigh_before_frontier():
|
|
|
|
|
"""A failed Sol/high plan gets its retained reasoning path before Astra."""
|
|
|
|
|
previous = lanes.Route(
|
|
|
|
|
"codex", "gpt-5.6-sol", "high", "codex-high", "classifier", "plan", 1, (), "advanced"
|
|
|
|
|
)
|
|
|
|
|
repeated = lanes.Route(
|
|
|
|
|
"codex", "gpt-5.6-sol", "high", "codex-high", "classifier", "same plan", 1, (), "advanced"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert lanes._quality_retry_assignee(previous, repeated) == "cli-codex-advanced-xhigh"
|
|
|
|
|
exhausted = lanes.Route(
|
|
|
|
|
"codex", "gpt-5.6-sol", "xhigh", "codex-xhigh", "classifier", "same plan", 1, (), "advanced"
|
|
|
|
|
)
|
|
|
|
|
assert lanes._quality_retry_assignee(exhausted, exhausted) == "cli-codex-frontier-xhigh"
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 08:16:35 -03:00
|
|
|
def test_missing_claim_is_a_noop(tmp_path: Path, monkeypatch):
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: None,
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
lanes.execute_claim("cassandra", "missing")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_transient_callback_storage_errors_do_not_kill_provider(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="t_callbacks",
|
|
|
|
|
status="running",
|
|
|
|
|
current_run_id=20,
|
|
|
|
|
assignee="cli-auto",
|
|
|
|
|
max_runtime_seconds=60,
|
|
|
|
|
)
|
|
|
|
|
blocks = []
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: task,
|
|
|
|
|
worker_log_path=lambda *_args, **_kwargs: tmp_path / "worker.log",
|
|
|
|
|
_resolve_worktree_workspace=lambda *_args, **_kwargs: (tmp_path, "branch"),
|
|
|
|
|
set_branch_name=lambda *_args: None,
|
|
|
|
|
set_workspace_path=lambda *_args: None,
|
|
|
|
|
build_worker_context=lambda *_args: "exercise callbacks",
|
|
|
|
|
heartbeat_worker=lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
|
|
|
sqlite3.OperationalError("heartbeat volume")
|
|
|
|
|
),
|
|
|
|
|
add_comment=lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
|
|
|
sqlite3.OperationalError("comment volume")
|
|
|
|
|
),
|
|
|
|
|
block_task=lambda *_args, **kwargs: blocks.append(kwargs),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "lanes")
|
|
|
|
|
monkeypatch.setattr(lanes, "KANBAN_STORAGE_ATTEMPTS", 1)
|
hermes: test quota gating, cooldown recovery, and failover policy
Deterministic coverage for the quota-aware lane: threshold boundaries
(14.9/15/15.1), both-below preference, fetch-failure fail-open, cooldown
elapsed-vs-not hysteresis, quota-reset recovery (never for auth),
explicit fail-closed in both directions, bounded double-failure block,
failure-reason classification, metrics emission, and worker env key
stripping. Based on PR #15 (fix/hermes-result-decomposition-reliability).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 20:31:12 -03:00
|
|
|
monkeypatch.setattr(lanes, "fresh_unavailable_provider", lambda *_args, **_kwargs: "claude")
|
2026-08-17 08:16:35 -03:00
|
|
|
route = lanes.Route("codex", "gpt", "high", "p", "c", "r", 1, ())
|
|
|
|
|
monkeypatch.setattr(lanes, "select_route", lambda *_args, **_kwargs: route)
|
|
|
|
|
|
|
|
|
|
def provider(*args, **_kwargs):
|
|
|
|
|
assert args[6]("still working") is True
|
2026-08-18 02:00:19 -03:00
|
|
|
return lanes.ProcessResult(1, "failed", None, False)
|
2026-08-17 08:16:35 -03:00
|
|
|
|
|
|
|
|
monkeypatch.setattr(lanes, "run_provider", provider)
|
|
|
|
|
lanes.execute_claim("cassandra", "t_callbacks")
|
|
|
|
|
|
|
|
|
|
assert blocks and blocks[0]["kind"] == "capability"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_restart_handoff_survives_missing_prior_log(tmp_path: Path, monkeypatch):
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="t_restart",
|
|
|
|
|
status="running",
|
|
|
|
|
current_run_id=21,
|
|
|
|
|
assignee="cli-auto",
|
|
|
|
|
max_runtime_seconds=60,
|
|
|
|
|
)
|
|
|
|
|
state_root = tmp_path / "lanes"
|
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", state_root)
|
|
|
|
|
state_file = lanes.state_path("cassandra", "t_restart")
|
|
|
|
|
lanes.atomic_json(state_file, {"current_route": {"provider": "claude"}})
|
|
|
|
|
blocks = []
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: task,
|
|
|
|
|
worker_log_path=lambda *_args, **_kwargs: tmp_path / "missing.log",
|
|
|
|
|
_resolve_worktree_workspace=lambda *_args, **_kwargs: (tmp_path, "branch"),
|
|
|
|
|
set_branch_name=lambda *_args: None,
|
|
|
|
|
set_workspace_path=lambda *_args: None,
|
|
|
|
|
build_worker_context=lambda *_args: "resume",
|
|
|
|
|
add_comment=lambda *_args, **_kwargs: None,
|
|
|
|
|
heartbeat_worker=lambda *_args, **_kwargs: True,
|
|
|
|
|
block_task=lambda *_args, **kwargs: blocks.append(kwargs),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
route = lanes.Route("codex", "gpt", "high", "p", "c", "r", 1, ())
|
|
|
|
|
monkeypatch.setattr(lanes, "select_route", lambda *_args, **_kwargs: route)
|
|
|
|
|
handoffs = []
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"git_handoff",
|
|
|
|
|
lambda _workspace, output: handoffs.append(output) or "handoff",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"run_provider",
|
|
|
|
|
lambda *_args, **_kwargs: lanes.ProcessResult(1, "failed", None, False),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lanes.execute_claim("cassandra", "t_restart")
|
|
|
|
|
|
|
|
|
|
assert handoffs == ["Previous provider log was unavailable after restart."]
|
|
|
|
|
assert blocks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unreplayable_persistence_error_recovers_exact_run(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="t_persist",
|
|
|
|
|
status="running",
|
|
|
|
|
current_run_id=22,
|
|
|
|
|
assignee="cli-auto",
|
|
|
|
|
max_runtime_seconds=60,
|
|
|
|
|
)
|
|
|
|
|
comments = []
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: task,
|
|
|
|
|
worker_log_path=lambda *_args, **_kwargs: tmp_path / "worker.log",
|
|
|
|
|
_resolve_worktree_workspace=lambda *_args, **_kwargs: (tmp_path, "branch"),
|
|
|
|
|
set_branch_name=lambda *_args: None,
|
|
|
|
|
set_workspace_path=lambda *_args: None,
|
|
|
|
|
build_worker_context=lambda *_args: "finish",
|
|
|
|
|
add_comment=lambda _conn, _task, _author, body: comments.append(body),
|
|
|
|
|
heartbeat_worker=lambda *_args, **_kwargs: True,
|
|
|
|
|
block_task=lambda *_args, **_kwargs: None,
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "lanes")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"select_route",
|
|
|
|
|
lambda *_args, **_kwargs: lanes.Route(
|
|
|
|
|
"codex", "gpt", "high", "p", "c", "r", 1, ()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"run_provider",
|
|
|
|
|
lambda *_args, **_kwargs: lanes.ProcessResult(
|
|
|
|
|
0, "", _completed_result("accepted"), False
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"_write_terminal_record",
|
|
|
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("disk")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(lanes, "_has_pending_finalization", lambda *_args: False)
|
|
|
|
|
recovered = []
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"_recover_exact_run",
|
|
|
|
|
lambda *args: recovered.append(args) or True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lanes.execute_claim("cassandra", "t_persist")
|
|
|
|
|
|
|
|
|
|
assert recovered and recovered[0][1].run_id == 22
|
|
|
|
|
assert any("terminal replayable=False" in body for body in comments)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unexpected_route_exception_blocks_the_exact_run(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="t_route",
|
|
|
|
|
status="running",
|
|
|
|
|
current_run_id=23,
|
|
|
|
|
assignee="cli-auto",
|
|
|
|
|
max_runtime_seconds=60,
|
|
|
|
|
)
|
|
|
|
|
blocks = []
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: task,
|
|
|
|
|
worker_log_path=lambda *_args, **_kwargs: tmp_path / "worker.log",
|
|
|
|
|
_resolve_worktree_workspace=lambda *_args, **_kwargs: (tmp_path, "branch"),
|
|
|
|
|
set_branch_name=lambda *_args: None,
|
|
|
|
|
set_workspace_path=lambda *_args: None,
|
|
|
|
|
build_worker_context=lambda *_args: "route",
|
|
|
|
|
add_comment=lambda *_args, **_kwargs: None,
|
|
|
|
|
heartbeat_worker=lambda *_args, **_kwargs: True,
|
|
|
|
|
block_task=lambda *_args, **kwargs: blocks.append(kwargs),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "lanes")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"select_route",
|
|
|
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("router down")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lanes.execute_claim("cassandra", "t_route")
|
|
|
|
|
|
|
|
|
|
assert blocks[0]["expected_run_id"] == 23
|
|
|
|
|
assert "router down" in blocks[0]["reason"]
|
2026-08-17 15:03:59 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_capacity_fallback_without_evidence_blocks_with_provider_output(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
"""A fallback turn with no structured result blocks on raw provider output."""
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="t_nofallback",
|
|
|
|
|
status="running",
|
|
|
|
|
current_run_id=22,
|
|
|
|
|
assignee="cli-auto",
|
|
|
|
|
max_runtime_seconds=60,
|
|
|
|
|
)
|
|
|
|
|
blocks = []
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: task,
|
|
|
|
|
worker_log_path=lambda *_args, **_kwargs: tmp_path / "worker.log",
|
|
|
|
|
_resolve_worktree_workspace=lambda *_args, **_kwargs: (tmp_path, "branch"),
|
|
|
|
|
set_branch_name=lambda *_args: None,
|
|
|
|
|
set_workspace_path=lambda *_args: None,
|
|
|
|
|
build_worker_context=lambda *_args: "attempt both providers",
|
|
|
|
|
add_comment=lambda *_args, **_kwargs: None,
|
|
|
|
|
heartbeat_worker=lambda *_args, **_kwargs: True,
|
|
|
|
|
block_task=lambda *_args, **kwargs: blocks.append(kwargs),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "lanes")
|
hermes: test quota gating, cooldown recovery, and failover policy
Deterministic coverage for the quota-aware lane: threshold boundaries
(14.9/15/15.1), both-below preference, fetch-failure fail-open, cooldown
elapsed-vs-not hysteresis, quota-reset recovery (never for auth),
explicit fail-closed in both directions, bounded double-failure block,
failure-reason classification, metrics emission, and worker env key
stripping. Based on PR #15 (fix/hermes-result-decomposition-reliability).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 20:31:12 -03:00
|
|
|
monkeypatch.setattr(lanes, "fresh_unavailable_provider", lambda *_args, **_kwargs: None)
|
2026-08-17 15:03:59 -03:00
|
|
|
codex = lanes.Route("codex", "gpt", "high", "p", "c", "r", 1, ())
|
|
|
|
|
claude = lanes.Route("claude", "fable", "high", "p", "c", "r", 1, ())
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"select_route",
|
|
|
|
|
lambda _prompt, assignee, **_kwargs: claude
|
|
|
|
|
if assignee == "cli-claude-high"
|
|
|
|
|
else codex,
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(lanes, "git_handoff", lambda *_args: "handoff")
|
|
|
|
|
reports = [
|
|
|
|
|
lanes.ProcessResult(1, "usage limit reached", None, True),
|
|
|
|
|
lanes.ProcessResult(1, "fallback also failed", None, False),
|
|
|
|
|
]
|
|
|
|
|
monkeypatch.setattr(lanes, "run_provider", lambda *_args, **_kwargs: reports.pop(0))
|
|
|
|
|
|
|
|
|
|
lanes.execute_claim("cassandra", "t_nofallback")
|
|
|
|
|
|
|
|
|
|
assert reports == []
|
|
|
|
|
assert blocks and "fallback also failed" in blocks[0]["reason"]
|
|
|
|
|
assert blocks[0]["kind"] == "capability"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_worker_blockers_become_the_block_reason_without_goal_mode(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
"""A structured non-completed report blocks with its own stated blockers."""
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="t_blocked",
|
|
|
|
|
status="running",
|
|
|
|
|
current_run_id=23,
|
|
|
|
|
assignee="cli-auto",
|
|
|
|
|
max_runtime_seconds=60,
|
|
|
|
|
)
|
|
|
|
|
blocks = []
|
|
|
|
|
db = SimpleNamespace(
|
|
|
|
|
scoped_current_board=lambda _board: nullcontext(),
|
|
|
|
|
connect=lambda board: _Connection(),
|
|
|
|
|
get_task=lambda *_args: task,
|
|
|
|
|
worker_log_path=lambda *_args, **_kwargs: tmp_path / "worker.log",
|
|
|
|
|
_resolve_worktree_workspace=lambda *_args, **_kwargs: (tmp_path, "branch"),
|
|
|
|
|
set_branch_name=lambda *_args: None,
|
|
|
|
|
set_workspace_path=lambda *_args: None,
|
|
|
|
|
build_worker_context=lambda *_args: "report the obstacle",
|
|
|
|
|
add_comment=lambda *_args, **_kwargs: None,
|
|
|
|
|
heartbeat_worker=lambda *_args, **_kwargs: True,
|
|
|
|
|
block_task=lambda *_args, **kwargs: blocks.append(kwargs),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=db))
|
|
|
|
|
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "lanes")
|
hermes: test quota gating, cooldown recovery, and failover policy
Deterministic coverage for the quota-aware lane: threshold boundaries
(14.9/15/15.1), both-below preference, fetch-failure fail-open, cooldown
elapsed-vs-not hysteresis, quota-reset recovery (never for auth),
explicit fail-closed in both directions, bounded double-failure block,
failure-reason classification, metrics emission, and worker env key
stripping. Based on PR #15 (fix/hermes-result-decomposition-reliability).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 20:31:12 -03:00
|
|
|
monkeypatch.setattr(lanes, "fresh_unavailable_provider", lambda *_args, **_kwargs: None)
|
2026-08-17 15:03:59 -03:00
|
|
|
route = lanes.Route("codex", "gpt", "high", "p", "c", "r", 1, ())
|
|
|
|
|
monkeypatch.setattr(lanes, "select_route", lambda *_args, **_kwargs: route)
|
|
|
|
|
structured = {
|
|
|
|
|
**_completed_result("progress only"),
|
|
|
|
|
"status": "blocked",
|
|
|
|
|
"blockers": ["credentials expired", "remote unreachable"],
|
|
|
|
|
}
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lanes,
|
|
|
|
|
"run_provider",
|
|
|
|
|
lambda *_args, **_kwargs: lanes.ProcessResult(0, "out", structured, False),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lanes.execute_claim("cassandra", "t_blocked")
|
|
|
|
|
|
|
|
|
|
assert blocks and blocks[0]["reason"] == (
|
|
|
|
|
"credentials expired; remote unreachable"
|
|
|
|
|
)
|