Files
codex-app-server/cloud/tests/test_playwright_bridge.py
host 910168126d perf: 桥变化才写、stdin 优先、stream 50ms 轮询
1. 桥写放大:旧代码每 ~50ms 轮询都无条件重写 state 和帧
   文件(约 40 次原子 temp+replace/s 走 runsc gofer,
   泵持久返回同一帧、updated_at 每轮都变,字节其实不变)。
   现帧字节不变不写;state 只在 url/title/started/holder/
   streaming 变化时写,或 0.5s 心跳一次(判死阈值 30s,
   绰绰有余);静态页写量从 ~40次/s 降到 ~2次/s。
2. MCP 主循环 stdin 优先:旧循环先 drain 再读 stdin,
   每次工具调用都先付一轮文件 IO。现 pending 请求先处理,
   空闲轮询 drain,忙时 200ms 保底 drain 一次防饿死。
3. stream 轮询 200ms → 50ms,画面延迟下限随之下降。
附回归测试:无变化轮询不重写、帧变化立即写、holder
变化绕过节流立即写。
2026-10-30 13:36:24 +08:00

202 lines
7.3 KiB
Python

"""File-bridge remote control tests (no Chromium, no sockets).
The bridge is four regular files in the worktree because runsc
containers cannot publish a Unix socket to the host (verified
2026-10-30). These tests drive RemoteBridge.poll() with a stub
browser and plain files.
"""
import json
import os
import sys
import tempfile
import time
import unittest
from pathlib import Path
try:
from playwright_remote import (
RemoteBridge, append_input, effective_holder, read_state,
validate_input, write_control)
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.playwright_remote import (
RemoteBridge, append_input, effective_holder, read_state,
validate_input, write_control)
class _StubBrowser:
def __init__(self):
self.calls = []
self.frame = ""
def cached_info(self):
return {"url": "https://example.com", "title": "T",
"started": True}
def click_at(self, x, y):
self.calls.append(("click_at", x, y))
return {"ok": True}
def mouse_move(self, x, y):
self.calls.append(("mouse_move", x, y))
return {"ok": True}
def key_press(self, key):
self.calls.append(("key", key))
return {"ok": True}
def type_text(self, text):
self.calls.append(("type", text))
return {"ok": True}
def pump_screencast(self):
return self.frame
class BridgeTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.browser = _StubBrowser()
self.bridge = RemoteBridge(self.browser, self.tmp.name)
def tearDown(self):
self.tmp.cleanup()
def test_control_and_input_flow(self):
# Agent holder: inputs are dropped, not executed.
append_input(self.tmp.name,
{"type": "mouse_click", "x": 10, "y": 20})
self.bridge.poll()
self.assertEqual(self.browser.calls, [])
# Takeover via control file, then a fresh input executes.
write_control(self.tmp.name, "user")
self.bridge.poll()
self.assertEqual(self.bridge.get_holder(), "user")
append_input(self.tmp.name,
{"type": "mouse_click", "x": 30, "y": 40})
self.bridge.poll()
self.assertEqual(self.browser.calls, [("click_at", 30, 40)])
# Second poll re-executes nothing (offset tracking).
self.bridge.poll()
self.assertEqual(self.browser.calls, [("click_at", 30, 40)])
# Release drops later inputs until the next takeover.
write_control(self.tmp.name, "agent")
self.bridge.poll()
append_input(self.tmp.name, {"type": "key", "key": "Enter"})
self.bridge.poll()
self.assertEqual(self.browser.calls, [("click_at", 30, 40)])
def test_partial_line_waits(self):
write_control(self.tmp.name, "user")
self.bridge.poll()
with open(os.path.join(self.tmp.name,
".browser-remote-input.jsonl"),
"ab") as fh:
fh.write(b'{"type": "key", "key": "En')
self.bridge.poll()
self.assertEqual(self.browser.calls, [])
with open(os.path.join(self.tmp.name,
".browser-remote-input.jsonl"),
"ab") as fh:
fh.write(b'ter"}\n')
self.bridge.poll()
self.assertEqual(self.browser.calls, [("key", "Enter")])
def test_state_and_frame_files(self):
self.browser.frame = "FRAMEBYTES"
self.bridge.poll()
state, err = read_state(self.tmp.name)
self.assertIsNone(err)
self.assertEqual(state["holder"], "agent")
self.assertTrue(state["streaming"])
self.assertGreater(state["updated_at"], time.time() - 5)
with open(os.path.join(self.tmp.name,
".browser-remote-frame.jpg"),
"rb") as fh:
self.assertEqual(fh.read(), b"FRAMEBYTES")
def test_writes_are_change_gated(self):
"""Unchanged polls must not rewrite the bridge files.
The old code rewrote both files every ~50ms iteration
(~40 atomic temp+replace round-trips per second through
the runsc gofer mount for identical bytes). Now only
changed bytes and the heartbeat get written.
"""
state_path = os.path.join(
self.tmp.name, ".browser-remote-state.json")
frame_path = os.path.join(
self.tmp.name, ".browser-remote-frame.jpg")
self.browser.frame = "FRAMEBYTES"
self.bridge.poll()
old = time.time() - 100
os.utime(state_path, (old, old))
os.utime(frame_path, (old, old))
# Unchanged second poll: neither file rewritten.
self.bridge.poll()
self.assertEqual(os.stat(state_path).st_mtime, old)
self.assertEqual(os.stat(frame_path).st_mtime, old)
# Changed frame: rewritten at once.
self.browser.frame = "NEWBYTES"
self.bridge.poll()
with open(frame_path, "rb") as fh:
self.assertEqual(fh.read(), b"NEWBYTES")
# Changed holder: state rewritten at once, bypassing
# the heartbeat throttle.
os.utime(state_path, (old, old))
write_control(self.tmp.name, "user")
self.bridge.poll()
state, err = read_state(self.tmp.name)
self.assertIsNone(err)
self.assertEqual(state["holder"], "user")
def test_read_state_missing_and_stale(self):
state, err = read_state(self.tmp.name)
self.assertEqual(err[0], 503)
self.bridge.poll()
path = os.path.join(self.tmp.name, ".browser-remote-state.json")
with open(path, "rb") as fh:
doc = json.loads(fh.read())
doc["updated_at"] = time.time() - 120
with open(path, "wb") as fh:
fh.write(json.dumps(doc).encode())
state, err = read_state(self.tmp.name)
self.assertEqual(err[0], 503)
def test_effective_holder_control_race(self):
# State file says agent; a newer control file wins.
self.bridge.poll()
state, _ = read_state(self.tmp.name)
self.assertEqual(effective_holder(self.tmp.name, state),
"agent")
write_control(self.tmp.name, "user")
self.assertEqual(effective_holder(self.tmp.name, state),
"user")
# Once the MCP polls, the state file catches up and both
# agree again.
self.bridge.poll()
state, _ = read_state(self.tmp.name)
self.assertEqual(state["holder"], "user")
self.assertEqual(effective_holder(self.tmp.name, state),
"user")
def test_validate_input(self):
item, err = validate_input({"type": "mouse_click", "x": 1,
"y": 2})
self.assertIsNone(err)
_, err = validate_input({"type": "mouse_click", "x": 2000,
"y": 2})
self.assertEqual(err[0], 400)
_, err = validate_input({"type": "mouse_click", "x": 1.5,
"y": 2})
self.assertEqual(err[0], 400)
_, err = validate_input({"type": "teleport"})
self.assertEqual(err[0], 400)
_, err = validate_input("nope")
self.assertEqual(err[0], 400)
if __name__ == "__main__":
unittest.main()