Files
pr-agent-server/scripts/smoke_hermes_api_server.py
asepharyana 66b567a6bc feat(worker): per-fork session header + 3600s sync timeout + merge salvage
- _hermes_api_post sends X-Hermes-Session-Id so each fork sync uses a
  fresh gateway transcript (was: derived session reused 200k-token history
  across upstream tips, making later runs pay growing context)
- SYNC_CLAUDE_TIMEOUT 2700→3600: a fresh session needs ~25min/120 calls
  to resolve 17 files + verify + commit; 2700s cut the HTTP call while the
  agent was still committing
- salvage-on-timeout: when the agent call ends early (timeout/transport)
  but the merge is already committed (MERGE_HEAD gone), finish+push it
  instead of aborting — verified live: 17 conflicts resolved by agent,
  push ok, fork 27 ahead / 0 behind
- _sync_finish_merge blocks committing files that still carry conflict
  markers (guard against git add of unresolved files)
- tests: 68/68 (session header, salvage completed merge, abort unfinished,
  marker guard; fake-leak hardening in _install_git_fake)
2026-09-21 21:02:33 +07:00

99 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Standalone smoke test for the Hermes gateway API server adapter.
Starts APIServerAdapter on a private port (default 8643) using the LIVE hermes
install, then drives a real agent turn through POST /v1/chat/completions —
proving the whole path the pr-queue-worker depends on:
bearer auth -> route -> AIAgent -> toolset (terminal/file) -> response
No gateway restart needed and nothing in the running gateway is touched.
"""
import argparse
import asyncio
import json
import os
import sys
import time
INSTALL = os.environ.get("HERMES_INSTALL", "/home/code/.hermes/hermes-agent")
sys.path.insert(0, INSTALL)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8643)
ap.add_argument("--key", default="standalone-smoke-key-0123456789abcdef")
ap.add_argument("--prompt", default=None)
ap.add_argument("--timeout", type=float, default=300.0)
ap.add_argument("--keep-alive", action="store_true", help="serve until Ctrl-C")
args = ap.parse_args()
os.environ["API_SERVER_KEY"] = args.key
os.environ["API_SERVER_HOST"] = "127.0.0.1"
os.environ["API_SERVER_PORT"] = str(args.port)
from gateway.config import PlatformConfig, Platform
from gateway.platforms.api_server import APIServerAdapter
cfg = PlatformConfig(enabled=True, extra={"host": "127.0.0.1", "port": args.port, "key": args.key})
adapter = APIServerAdapter(cfg)
print(f"[smoke] starting API server adapter on 127.0.0.1:{args.port}", flush=True)
async def run():
ok = await adapter.connect()
print(f"[smoke] connect() -> {ok}", flush=True)
if not ok:
err = getattr(adapter, "_fatal_error", None) or getattr(adapter, "fatal_error", None)
print(f"[smoke] fatal: {err}", flush=True)
return 1
if args.keep_alive:
print("[smoke] serving — Ctrl-C to stop", flush=True)
while True:
await asyncio.sleep(3600)
await asyncio.sleep(0.5)
import httpx
prompt = args.prompt or (
"Use your terminal tool to run exactly: echo HERMES_API_OK && pwd\n"
"Then reply with the raw output only."
)
body = {
"model": "hermes-agent",
"messages": [{"role": "user", "content": prompt}],
"stream": False,
}
url = f"http://127.0.0.1:{args.port}/v1/chat/completions"
t0 = time.time()
try:
async with httpx.AsyncClient(timeout=args.timeout) as client:
r = await client.post(url, headers={"Authorization": f"Bearer {args.key}"}, json=body)
dt = time.time() - t0
print(f"[smoke] HTTP {r.status_code} in {dt:.1f}s", flush=True)
print("[smoke] body:", r.text[:1500], flush=True)
if r.status_code != 200:
return 1
data = r.json()
text = (data.get("choices") or [{}])[0].get("message", {}).get("content", "")
print(f"[smoke] agent text: {text[:400]!r}", flush=True)
if "HERMES_API_OK" not in text:
print("[smoke] FAIL: terminal tool output not present in the answer", flush=True)
return 1
print("[smoke] PASS: agent ran a real terminal tool through the API server", flush=True)
return 0
except Exception as exc:
print(f"[smoke] request failed: {type(exc).__name__}: {exc}", flush=True)
return 1
try:
return asyncio.run(run())
finally:
try:
asyncio.run(adapter.disconnect())
except Exception:
pass
if __name__ == "__main__":
sys.exit(main())