237 lines
5.8 KiB
Python
237 lines
5.8 KiB
Python
|
|
"""Focused tests for Agent Hermes' direct Codex and Claude Kanban lanes."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import errno
|
||
|
|
|
||
|
|
import importlib.util
|
||
|
|
|
||
|
|
import hashlib
|
||
|
|
|
||
|
|
import json
|
||
|
|
|
||
|
|
import os
|
||
|
|
|
||
|
|
import signal
|
||
|
|
|
||
|
|
import stat
|
||
|
|
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import threading
|
||
|
|
|
||
|
|
import time
|
||
|
|
|
||
|
|
from contextlib import nullcontext
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from types import SimpleNamespace
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
SCRIPTS = Path(__file__).parents[2] / "services/hermes/scripts"
|
||
|
|
|
||
|
|
sys.path.insert(0, str(SCRIPTS))
|
||
|
|
|
||
|
|
HERMES = Path(__file__).parents[2] / "services/hermes"
|
||
|
|
|
||
|
|
KEYCLOAK = Path(__file__).parents[2] / "services/keycloak"
|
||
|
|
|
||
|
|
FLUX_HERMES = (
|
||
|
|
Path(__file__).parents[2]
|
||
|
|
/ "clusters/atlas/flux-system/applications/hermes/kustomization.yaml"
|
||
|
|
)
|
||
|
|
|
||
|
|
def _load(name: str):
|
||
|
|
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
|
||
|
|
assert spec and spec.loader
|
||
|
|
module = importlib.util.module_from_spec(spec)
|
||
|
|
sys.modules[spec.name] = module
|
||
|
|
spec.loader.exec_module(module)
|
||
|
|
return module
|
||
|
|
|
||
|
|
_lane_facade = _load("cli_lane_runner")
|
||
|
|
|
||
|
|
|
||
|
|
class _LaneSurface:
|
||
|
|
"""Patch every module-local import behind the compatibility facade."""
|
||
|
|
|
||
|
|
def __init__(self, facade):
|
||
|
|
object.__setattr__(self, "_facade", facade)
|
||
|
|
object.__setattr__(
|
||
|
|
self,
|
||
|
|
"_modules",
|
||
|
|
tuple(
|
||
|
|
module
|
||
|
|
for name, module in sys.modules.items()
|
||
|
|
if name == "cli_lane_runner" or name.startswith("cli_lane_")
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
def __getattr__(self, name):
|
||
|
|
facade = object.__getattribute__(self, "_facade")
|
||
|
|
if hasattr(facade, name):
|
||
|
|
return getattr(facade, name)
|
||
|
|
for module in object.__getattribute__(self, "_modules"):
|
||
|
|
if hasattr(module, name):
|
||
|
|
return getattr(module, name)
|
||
|
|
raise AttributeError(name)
|
||
|
|
|
||
|
|
def __setattr__(self, name, value):
|
||
|
|
matched = False
|
||
|
|
for module in object.__getattribute__(self, "_modules"):
|
||
|
|
if hasattr(module, name):
|
||
|
|
setattr(module, name, value)
|
||
|
|
matched = True
|
||
|
|
if not matched:
|
||
|
|
setattr(object.__getattribute__(self, "_facade"), name, value)
|
||
|
|
|
||
|
|
|
||
|
|
lanes = _LaneSurface(_lane_facade)
|
||
|
|
|
||
|
|
policy = _load("claude_command_policy")
|
||
|
|
|
||
|
|
migration = _load("migrate_herdr_state")
|
||
|
|
|
||
|
|
auth_patch = _load("patch_hermes_auth")
|
||
|
|
|
||
|
|
tui_gateway_patch = _load("patch_tui_gateway")
|
||
|
|
|
||
|
|
codex_runtime_patch = _load("patch_codex_runtime")
|
||
|
|
|
||
|
|
ttyd_patch = _load("patch_ttyd_index")
|
||
|
|
|
||
|
|
client_config = _load("configure_agent_clients")
|
||
|
|
|
||
|
|
def _agent_deployment() -> dict:
|
||
|
|
return yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
|
||
|
|
|
||
|
|
def _services() -> dict[str, dict]:
|
||
|
|
return {
|
||
|
|
item["metadata"]["name"]: item
|
||
|
|
for item in yaml.safe_load_all((HERMES / "service.yaml").read_text())
|
||
|
|
if item
|
||
|
|
}
|
||
|
|
|
||
|
|
def _oauth_deployment(name: str) -> dict:
|
||
|
|
documents = [
|
||
|
|
item
|
||
|
|
for item in yaml.safe_load_all((HERMES / "oauth2-proxy.yaml").read_text())
|
||
|
|
if item
|
||
|
|
]
|
||
|
|
return next(
|
||
|
|
item
|
||
|
|
for item in documents
|
||
|
|
if item["kind"] == "Deployment" and item["metadata"]["name"] == name
|
||
|
|
)
|
||
|
|
|
||
|
|
class _SwitchyardResponse:
|
||
|
|
"""Minimal context-managed response used by routing contract tests."""
|
||
|
|
|
||
|
|
def __init__(self, selected: str, rationale: str = "local classifier vote"):
|
||
|
|
self.headers = {
|
||
|
|
"x-model-router-selected-model": selected,
|
||
|
|
"x-model-router-rationale": rationale,
|
||
|
|
}
|
||
|
|
|
||
|
|
def __enter__(self):
|
||
|
|
return self
|
||
|
|
|
||
|
|
def __exit__(self, *_args):
|
||
|
|
return False
|
||
|
|
|
||
|
|
def read(self):
|
||
|
|
return b"{}"
|
||
|
|
|
||
|
|
def _completed_result(summary: str = "done") -> dict:
|
||
|
|
return {
|
||
|
|
"status": "completed",
|
||
|
|
"summary": summary,
|
||
|
|
"changed_files": [],
|
||
|
|
"tests_run": [],
|
||
|
|
"artifacts": [],
|
||
|
|
"findings": [],
|
||
|
|
"blockers": [],
|
||
|
|
}
|
||
|
|
|
||
|
|
def _pending_terminal_record(board: str, task_id: str, run_id: int, summary: str) -> dict:
|
||
|
|
structured = _completed_result(summary)
|
||
|
|
return {
|
||
|
|
"board": board,
|
||
|
|
"task_id": task_id,
|
||
|
|
"expected_run_id": run_id,
|
||
|
|
"result": json.dumps(structured, sort_keys=True),
|
||
|
|
"summary": summary,
|
||
|
|
"metadata": {},
|
||
|
|
"kanban_state": "pending",
|
||
|
|
"recorded_at": lanes.utc_now(),
|
||
|
|
}
|
||
|
|
|
||
|
|
def _install_terminal_recovery_db(monkeypatch, task, completions, reclaims) -> None:
|
||
|
|
class Connection:
|
||
|
|
def close(self):
|
||
|
|
return None
|
||
|
|
|
||
|
|
def complete_task(_conn, _task_id, **kwargs):
|
||
|
|
if task.status != "running" or task.current_run_id != kwargs["expected_run_id"]:
|
||
|
|
return False
|
||
|
|
task.status = "done"
|
||
|
|
task.result = kwargs["result"]
|
||
|
|
task.current_run_id = None
|
||
|
|
completions.append(kwargs["result"])
|
||
|
|
return True
|
||
|
|
|
||
|
|
def reclaim_task(_conn, _task_id, **kwargs):
|
||
|
|
reclaims.append(kwargs)
|
||
|
|
return False
|
||
|
|
|
||
|
|
fake_db = SimpleNamespace(
|
||
|
|
scoped_current_board=lambda _board: nullcontext(),
|
||
|
|
connect=lambda board: Connection(),
|
||
|
|
get_task=lambda _conn, _task_id: task,
|
||
|
|
complete_task=complete_task,
|
||
|
|
reclaim_task=reclaim_task,
|
||
|
|
)
|
||
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||
|
|
|
||
|
|
__all__ = [
|
||
|
|
'FLUX_HERMES',
|
||
|
|
'HERMES',
|
||
|
|
'KEYCLOAK',
|
||
|
|
'Path',
|
||
|
|
'SCRIPTS',
|
||
|
|
'SimpleNamespace',
|
||
|
|
'_SwitchyardResponse',
|
||
|
|
'_agent_deployment',
|
||
|
|
'_completed_result',
|
||
|
|
'_install_terminal_recovery_db',
|
||
|
|
'_load',
|
||
|
|
'_oauth_deployment',
|
||
|
|
'_pending_terminal_record',
|
||
|
|
'_services',
|
||
|
|
'auth_patch',
|
||
|
|
'client_config',
|
||
|
|
'codex_runtime_patch',
|
||
|
|
'errno',
|
||
|
|
'hashlib',
|
||
|
|
'importlib',
|
||
|
|
'json',
|
||
|
|
'lanes',
|
||
|
|
'migration',
|
||
|
|
'nullcontext',
|
||
|
|
'os',
|
||
|
|
'policy',
|
||
|
|
'pytest',
|
||
|
|
'signal',
|
||
|
|
'stat',
|
||
|
|
'sys',
|
||
|
|
'threading',
|
||
|
|
'time',
|
||
|
|
'ttyd_patch',
|
||
|
|
'tui_gateway_patch',
|
||
|
|
'yaml',
|
||
|
|
]
|