atlas-iac/testing/tests/test_hermes_execution_pool_support.py
Hermes Agent 5d963c9354 hermes: make pool lease recovery and release isolation safe
Independent review t_5975c06a blocked this branch on a P1: a Kanban write that
failed while a lease expired left a `lease_failed` row that was invisible to
every pass, immortal to garbage collection, and fatal to the coordinator. It
poisoned `reconcile()` forever with a conflicting-duplicate primary key,
produced a spurious capability `block_task` from `dispatch()`, and -- because
startup maintenance ran unguarded before the port bound, against a store on a
PVC -- crash-looped the coordinator with no automatic recovery.

`lease_failed` is now a retryable state that every maintenance pass drains, and
a row only reaches a terminal state on authoritative evidence about its exact
Kanban run, so nothing is collected before its outcome is known and nothing is
silently dropped. Each row, task, and board is processed in isolation, and a
coordinator-side fault is never converted into a Kanban mutation. Startup runs
through the same guarded cycle as the steady-state loop.

The wire protocol and the durable store are now separate modules, and the
maintenance passes moved out of the coordinator, so each file stays under the
managed line ceiling with room for the recovery logic.

Also closes three consequential handoff risks the same review raised:

* mediator-N pinned itself hard to worker-N while sharing a ReadWriteOnce
  claim, so a drain or preemption that moved only the lower-priority worker
  deadlocked the ordinal on Multi-Attach until an operator deleted a Pod. The
  shared workspace is now ReadWriteMany (as the hermes-chat tenant workspaces
  already are on the same class), colocation is a preference, and the mediator
  shares the worker's preemption priority, so each Pod reschedules on its own.
* the broker permits only branch creation, so a retry that added commits could
  never submit and the run's work was discarded with the failure. Submission
  now targets a fresh attempt- or content-scoped ref in the same reviewed
  namespace -- never an update -- and is idempotent under replay. A refused
  submission downgrades the result and says why instead of unwinding the run.
* the provider CLIs were reinstalled into an emptyDir on every Pod start inside
  the 10m Flux health window for the whole hermes app. They now install once
  per pinned version onto a durable volume, re-verified against the real
  binaries and time-bounded, and the best-effort pool no longer gates the
  health of the app its dependents wait on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:31:15 +00:00

61 lines
1.8 KiB
Python

"""Shared fixtures for the execution-pool recovery and isolation suites."""
from __future__ import annotations
import sqlite3
import sys
import time
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[2]
SCRIPTS = ROOT / "services/hermes/scripts"
SCM_SCRIPTS = ROOT / "services/hermes/scm-common/scripts"
sys.path[:0] = [str(SCRIPTS), str(SCM_SCRIPTS)]
import execution_pool_maintenance as maintenance # noqa: E402
import execution_pool_store as pool_store # noqa: E402
from testing.tests.test_hermes_execution_pool_coordinator_v2 import ( # noqa: E402
assignment_payload,
binding,
)
LOCKED = sqlite3.OperationalError("database is locked")
@pytest.fixture(autouse=True)
def clear_pool_deferrals():
"""Keep the bounded deferral report from leaking between tests."""
maintenance.DEFERRALS.clear()
yield
maintenance.DEFERRALS.clear()
def exhausted_store(path, exact=None):
"""A store whose only assignment has burned every fenced attempt."""
store = pool_store.PoolStore(path)
store.add(exact or binding(attempt=3), assignment_payload())
store.offer(int((exact or binding(attempt=3))["worker_ordinal"]))
with store._connect() as connection:
connection.execute("UPDATE assignments SET lease_until=1")
return store
def states(store):
"""Every durable row as (run_id, ordinal, attempt, state)."""
return [
tuple(row)
for row in store._connect().execute(
"SELECT run_id,worker_ordinal,attempt,state FROM assignments ORDER BY run_id"
)
]
def age(store, seconds):
"""Backdate every row so retention-based collection can be exercised."""
with store._connect() as connection:
connection.execute("UPDATE assignments SET updated_at=?", (time.time() - seconds,))