"""Фейковый хаб для тестов агента: маршруты /agent/* в памяти.""" import json import re import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer class FakeHub: token = "agent-token" def __init__(self, settings): self.settings = dict(settings) self.queue = [] self.commands = {} self.results = {} self.states = [] self.known = [] self.claims = 0 self.fail_results = 0 self.reject_results = {} self.in_flight = None self.lock = threading.Lock() hub = self class Handler(BaseHTTPRequestHandler): def log_message(self, *a): pass def do_GET(self): hub.handle(self, "GET") def do_POST(self): hub.handle(self, "POST") self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) threading.Thread(target=self.server.serve_forever, daemon=True).start() @property def base_url(self): return "http://127.0.0.1:%d" % self.server.server_port def stop(self): self.server.shutdown() self.server.server_close() def enqueue(self, ctype, params, pause=0): cid = len(self.commands) + 1 self.commands[cid] = {"id": cid, "type": ctype, "params": params, "pause_seconds": pause, "status": "queued"} self.queue.append(cid) return cid def handle(self, h, method): length = int(h.headers.get("Content-Length") or 0) body = json.loads(h.rfile.read(length).decode("utf-8")) if length else {} if h.headers.get("Authorization") != "Bearer " + self.token: return self._send(h, 401, {"error": "unauthorized"}) with self.lock: code, payload = self.route(method, h.path.split("?")[0], body) self._send(h, code, payload) def _send(self, h, code, payload): data = json.dumps(payload, ensure_ascii=False).encode("utf-8") h.send_response(code) h.send_header("Content-Type", "application/json") h.send_header("Content-Length", str(len(data))) h.end_headers() h.wfile.write(data) def route(self, method, path, body): if path == "/agent/claim": self.claims += 1 if self.in_flight and self.commands[self.in_flight]["status"] == "claimed": return 200, {"command": None, "settings_version": self.settings["settings_version"]} if not self.queue: return 200, {"command": None, "settings_version": self.settings["settings_version"]} cid = self.queue.pop(0) self.commands[cid]["status"] = "claimed" self.in_flight = cid c = self.commands[cid] return 200, {"command": {"id": cid, "type": c["type"], "params": c["params"], "pause_seconds": c["pause_seconds"]}, "settings_version": self.settings["settings_version"]} if path == "/agent/settings": return 200, self.settings if path == "/agent/known-keys": return 200, {"keys": list(self.known)} if path == "/agent/state": self.states.append(body) return 200, {"issues": len(body.get("issues") or []), "links": len(body.get("links") or [])} m = re.match(r"^/agent/commands/(\d+)$", path) if m: c = self.commands.get(int(m.group(1))) return (200, c) if c else (404, {"error": "not_found"}) m = re.match(r"^/agent/commands/(\d+)/result$", path) if m: cid = int(m.group(1)) if self.fail_results > 0: self.fail_results -= 1 return 503, {"error": "busy"} if cid in self.reject_results: return self.reject_results[cid], {"error": "rejected"} if cid not in self.commands: return 404, {"error": "not_found"} if self.commands[cid]["status"] in ("done", "failed", "cancelled"): return 200, self.commands[cid] # терминальный статус не перезаписывается (как живой хаб) self.results[cid] = body self.commands[cid]["status"] = body.get("status") if self.in_flight == cid: self.in_flight = None return 200, self.commands[cid] return 404, {"error": "not_found", "detail": path}