#!/usr/bin/env python3 """ Does sending a chat message come back? Mounts **the real `ChatPanel` over the real `MeshBayTransport`** — both shipped modules, neither a model of the other — and types a message into the composer the way a person does. Only the DataChannel is replaced, by a stand-in that answers what the node answers. It exists because the defect it was written for is invisible to a structural test and to `chat_scroll_probe.py` alike: nothing in `chat-app.js` is wrong, and the transport routes every message it knows how to route. The node's reply to a chat message is a bare `{"type": "ack"}` that names no request, so it fell through to `_dispatch`'s arrival-order guess and was handed to whichever request happened to be waiting — a `media_meta_req` from the Videos tab, say, which the node never answered because it refused the file_id with a bare `error` that named no request either. The chat send then waited out its own 30s timeout with the composer disabled, so the tab looked frozen and the message never appeared, while the node had stored it all along. chat_send_probe.py It now drives two shapes of reply, because there were two ways for one to go astray and only the first was ever fixed: `ack` the node accepts the message and answers `{"type": "ack"}`, which names no request. Routed by request type since 2026-08-30. `error` the node *refuses* it — every failure in `_dispatch_message` ends at one catch-all sending `{"type": "error", "detail": "Request failed"}`, and 238 of this module's 240 error sends name nothing either. That reply reached no caller at all: it went to whatever request happened to be waiting, and the send sat out its own 30s timeout with the composer disabled. Both are run with an older request already pending — the condition that turns "guess by arrival order" from usually-right into wrong — and both must come back inside a second and a half. Prints JSON: `scenarios`, the state of the panel at each stage of each, and `log`, what the transport sent and how the deliberately-unanswered request ended up. """ import http.server import json import socketserver import subprocess import sys import tempfile import threading import time from pathlib import Path STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static" PORT = 8755 RECORDS = [] socketserver.TCPServer.allow_reuse_address = True PAGE = r"""

a group

""" class H(http.server.BaseHTTPRequestHandler): def log_message(self, *a): pass def do_POST(self): RECORDS.append(json.loads( self.rfile.read(int(self.headers["Content-Length"])).decode())) self.send_response(204) self.end_headers() def do_GET(self): if self.path == "/": body, ctype = PAGE.encode(), "text/html; charset=utf-8" else: path = (STATIC / self.path.lstrip("/")).resolve() if not str(path).startswith(str(STATIC)) or not path.is_file(): self.send_response(404) self.end_headers() return body = path.read_bytes() ctype = ("text/css" if path.suffix == ".css" else "text/javascript" if path.suffix == ".js" else "application/octet-stream") self.send_response(200) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def main() -> int: with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv: threading.Thread(target=srv.serve_forever, daemon=True).start() with tempfile.TemporaryDirectory() as profile: proc = subprocess.Popen( ["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox", f"--user-data-dir={profile}", "--window-size=1100,800", f"http://127.0.0.1:{PORT}/"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(300): if RECORDS: break time.sleep(0.1) proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() if not RECORDS: print(json.dumps({"error": "no measurement"}), file=sys.stderr) return 1 print(json.dumps(RECORDS[0], indent=1)) return 0 if __name__ == "__main__": raise SystemExit(main())