Files
site/tools/harness-cdp.py
T
teahubxyzandClaude Fable 5.1 1aeebac6f2 harness: a resume mode (autoplay blocked); docs for the state machine and hooks
tools/harness.sh resume seeds IndexedDB with a station and one rendered
track, resumes it with no user gesture and expects the TAP TO PLAY wait.
IndexedDB needs http, so the page is served from a temp folder; headless
Chrome's virtual time doesn't wait for IndexedDB, so this mode is driven in
real time over the DevTools protocol (tools/harness-cdp.py, stdlib only).
The loop harness now prints state.phase too.

ARCHITECTURE.md describes the machine as implemented; HANDOFF.md lists what
was found and left alone; STORAGE.md gains db.get and the tapes store (the
code already had both); README points at the new files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 06:01:48 +00:00

91 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""tools/harness-cdp.py CHROME URL [TIMEOUT_S] [CHROME_FLAG ...]
Runs a harness page in headless Chrome in real time and prints its #diag once it says END (or the timeout passes).
--dump-dom with --virtual-time-budget (the loop harness) is deterministic but IndexedDB answers in real time and
virtual time does not wait for it, so the resume harness is driven over the DevTools protocol instead: start Chrome
with a debugging port, attach to the page, poll Runtime.evaluate. Stdlib only (a 40-line websocket client)."""
import base64, json, os, re, socket, struct, subprocess, sys, tempfile, time, urllib.request
class WS:
def __init__(self, url):
m = re.match(r"ws://([^:/]+):(\d+)(/.*)", url)
self.s = socket.create_connection((m.group(1), int(m.group(2))), timeout=10)
key = base64.b64encode(os.urandom(16)).decode()
self.s.sendall((f"GET {m.group(3)} HTTP/1.1\r\nHost: {m.group(1)}:{m.group(2)}\r\nUpgrade: websocket\r\n"
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
buf = b""
while b"\r\n\r\n" not in buf: buf += self.s.recv(4096)
assert b" 101 " in buf.split(b"\r\n")[0], buf[:200]
self.buf = buf.split(b"\r\n\r\n", 1)[1]
self.n = 0
def _read(self, n):
while len(self.buf) < n: self.buf += self.s.recv(65536)
out, self.buf = self.buf[:n], self.buf[n:]
return out
def send(self, obj):
data = json.dumps(obj).encode()
head = bytes([0x81])
if len(data) < 126: head += bytes([0x80 | len(data)])
elif len(data) < 65536: head += bytes([0x80 | 126]) + struct.pack(">H", len(data))
else: head += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
mask = os.urandom(4)
self.s.sendall(head + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv(self):
while True:
b0, b1 = self._read(2)
n = b1 & 0x7F
if n == 126: n = struct.unpack(">H", self._read(2))[0]
elif n == 127: n = struct.unpack(">Q", self._read(8))[0]
if b1 & 0x80: self._read(4)
data = self._read(n)
op = b0 & 0x0F
if op == 1: return json.loads(data)
if op == 8: raise EOFError("closed")
def call(self, method, **params):
self.n += 1
self.send({"id": self.n, "method": method, "params": params})
while True:
m = self.recv()
if m.get("id") == self.n: return m.get("result", {})
def main():
chrome, url = sys.argv[1], sys.argv[2]
timeout = float(sys.argv[3]) if len(sys.argv) > 3 else 30
flags = sys.argv[4:]
prof = tempfile.mkdtemp()
p = subprocess.Popen([chrome, "--headless=new", "--disable-gpu", "--remote-debugging-port=0", f"--user-data-dir={prof}", *flags, "about:blank"],
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
port = None
for line in p.stderr:
m = re.search(r"DevTools listening on ws://127\.0\.0\.1:(\d+)/", line)
if m: port = m.group(1); break
if not port: print("no diag: Chrome gave no DevTools port"); p.kill(); return 1
try:
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
page = next(t for t in targets if t["type"] == "page")
ws = WS(page["webSocketDebuggerUrl"])
ws.call("Page.enable"); ws.call("Runtime.enable")
ws.call("Page.navigate", url=url)
t0 = time.time(); text = ""
while time.time() - t0 < timeout:
time.sleep(0.25)
r = ws.call("Runtime.evaluate", expression="(document.getElementById('diag')||{}).textContent||''", returnByValue=True)
text = r.get("result", {}).get("value", "") or ""
if "\nEND" in text or text.startswith("END") or text.startswith("TOPERR"): break
print(text if text else "no diag: the harness did not run")
finally:
p.kill()
subprocess.run(["rm", "-rf", prof])
return 0
if __name__ == "__main__":
sys.exit(main())