Files
codex-app-server/cloud/tests/test_coverage85_misc.py
T
host 39ec2b4435 v5: rebase 融合修复(main 演进 + 附录源同步)
- websearch.py:补回 import socket(栈 46ca59f 的 lint 清理在栈
  上无害,replay 到 main 后 main #274 的超时/DNS 显式报错
  代码引用 socket,缺 import 致 2 项 NameError;re 在 main
  与重排后树中均 0 引用,不补)
- 3 个 websearch fake 补 scope="general" 参数(main 的
  handle_request 现必传 scope,旧 fake 致 TypeError 被吞成
  isError,3 项失败)
- playwright 工具数 14→18(main 新增 4 个,与 main _TOOLS
  逐名一致、无重复)
- playwright 种子测试改按 main 安全升级规则:源脚本 v1→v2
  且 manifest 证明未改动才刷新;未知改动只上报跳过不覆盖
- 附录源同步:01/02/07 三个 roles 源文件与 SKILL.md 附录章
  做同一改写,check_appendix 来源一致性恢复
2026-11-03 02:49:26 +00:00

859 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""覆盖率 85% 第 4 步第 5 批:workers / autoscaler / playwright 长尾。
只动测试。workers 与 autoscaler 纯函数走真实 SQLite;
playwright Browser 走类自带的 browser_factory 注入点(假
playwright 对象),ensure_* 只写临时 codex_home,绝不启动
真实浏览器。autoscaler 的进程操作以 Popen 替身覆盖启动,
不拉真实 worker 子进程。
"""
import json
import os
import sqlite3
import tempfile
import time
import unittest
from unittest import mock
from cloud import autoscaler as autoscaler_mod
from cloud import playwright_browser as pw_mod
from cloud import workers as workers_mod
def _sqlite_db(path, tasks=True, workers=False):
conn = sqlite3.connect(path)
if tasks:
conn.execute(
"CREATE TABLE tasks(id TEXT PRIMARY KEY, status TEXT,"
" depends_on TEXT, executor TEXT, coordinator_parts TEXT,"
" last_heartbeat REAL)")
if workers:
conn.execute(
"CREATE TABLE workers(worker_id TEXT PRIMARY KEY,"
" executor TEXT, capacity INTEGER, running INTEGER,"
" last_seen REAL, created_at REAL)")
conn.commit()
return conn
class WorkersTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = os.path.join(self.tmp.name, "w.db")
def tearDown(self):
self.tmp.cleanup()
def test_threshold_override_and_env(self):
self.assertEqual(workers_mod.alive_threshold_seconds(30), 30)
self.assertEqual(workers_mod.alive_threshold_seconds("bogus"),
120)
with mock.patch.dict(os.environ,
{"CLOUD_WORKER_ALIVE_SECONDS": "bogus"}):
self.assertEqual(workers_mod.alive_threshold_seconds(), 120)
with mock.patch.dict(os.environ,
{"CLOUD_WORKER_ALIVE_SECONDS": "45"}):
self.assertEqual(workers_mod.alive_threshold_seconds(), 45)
def test_register_and_heartbeat_flows(self):
with self.assertRaises(ValueError):
workers_mod.register_worker(self.db, "")
with self.assertRaises(ValueError):
workers_mod.heartbeat_worker(self.db, None)
row = workers_mod.register_worker(self.db, "w1",
executor="docker", capacity=2,
running=1)
self.assertEqual(row["worker_id"], "w1")
self.assertEqual(row["executor"], "docker")
self.assertEqual(row["capacity"], 2)
self.assertEqual(row["running"], 1)
row = workers_mod.register_worker(self.db, "w1",
executor="local", capacity=3)
self.assertEqual(row["executor"], "local")
self.assertEqual(row["running"], 0)
# 心跳未给 executor/capacity 时保留旧值,只刷时间。
row = workers_mod.heartbeat_worker(self.db, "w1", running=2)
self.assertEqual(row["executor"], "local")
self.assertEqual(row["capacity"], 3)
self.assertEqual(row["running"], 2)
# 未注册 worker 的心跳即注册。
row = workers_mod.heartbeat_worker(self.db, "w2")
self.assertEqual(row["capacity"], 1)
self.assertEqual(row["executor"], None)
def test_list_alive_and_stale(self):
workers_mod.register_worker(self.db, "fresh")
workers_mod.register_worker(self.db, "stale")
conn = sqlite3.connect(self.db)
conn.execute("UPDATE workers SET last_seen = ?"
" WHERE worker_id = 'stale'",
(time.time() - 10000,))
conn.commit()
conn.close()
rows = workers_mod.list_workers(self.db, alive_seconds=120)
by_id = {r["worker_id"]: r for r in rows}
self.assertTrue(by_id["fresh"]["alive"])
self.assertFalse(by_id["stale"]["alive"])
self.assertEqual([r["worker_id"] for r in rows],
["fresh", "stale"])
def test_ensure_old_table_adds_running_column(self):
conn = sqlite3.connect(self.db)
conn.execute(
"CREATE TABLE workers(worker_id TEXT PRIMARY KEY,"
" executor TEXT, capacity INTEGER, last_seen REAL,"
" created_at REAL)")
conn.execute("INSERT INTO workers VALUES('old', 'docker',"
" 1, 1000, 1000)")
conn.commit()
conn.close()
workers_mod.ensure_workers_table(self.db)
conn = sqlite3.connect(self.db)
cols = [r[1] for r in
conn.execute("PRAGMA table_info(workers)").fetchall()]
conn.close()
self.assertIn("running", cols)
# 幂等:已含 running 列时再跑不报错。
workers_mod.ensure_workers_table(self.db)
def test_worker_to_dict_missing_keys(self):
d = workers_mod._worker_to_dict({"worker_id": "w"})
self.assertEqual(d["worker_id"], "w")
self.assertIsNone(d["executor"])
self.assertEqual(d["running"], 0)
d = workers_mod._worker_to_dict(None)
self.assertIsNone(d["worker_id"])
self.assertEqual(d["running"], 0)
class _FakeCursor:
def __init__(self, rows=None):
self.rows = rows or []
def fetchone(self):
return self.rows[0] if self.rows else None
def fetchall(self):
return self.rows
class _FakeConn:
"""workers 依赖桩:PRAGMA 抛错、information_schema 可控。"""
def __init__(self, info_row=None, count_row=None):
self.info_row = info_row
self.count_row = count_row
self.executed = []
def execute(self, sql, params=None):
self.executed.append(sql)
if "PRAGMA table_info" in sql:
raise RuntimeError("no pragma")
if "information_schema.columns" in sql and "COUNT" in sql:
return _FakeCursor([self.count_row] if self.count_row
else [])
if "information_schema.columns" in sql:
if self.info_row is None:
raise RuntimeError("no information_schema")
return _FakeCursor([self.info_row])
if "ALTER TABLE" in sql:
raise RuntimeError("alter not supported here")
return _FakeCursor()
def commit(self):
pass
def rollback(self):
pass
def close(self):
pass
class WorkersFakeConnTest(unittest.TestCase):
def test_ensure_running_column_pg_fallback(self):
conn = _FakeConn(count_row=(0,))
workers_mod._ensure_running_column(conn)
self.assertTrue(any("ADD COLUMN IF NOT EXISTS" in s
for s in conn.executed))
def test_ensure_running_column_pg_query_fails(self):
conn = _FakeConn()
# information_schema 两次查询都抛错:函数只回滚不抛。
workers_mod._ensure_running_column(conn)
def test_ensure_table_pg_real_column_path(self):
conn = _FakeConn(info_row=("real",))
with mock.patch.object(workers_mod, "_db_connect",
return_value=conn):
with mock.patch.object(workers_mod,
"_ensure_running_column"):
workers_mod.ensure_workers_table("fake.db")
self.assertTrue(any("ALTER TABLE workers ALTER COLUMN" in s
for s in conn.executed))
class AutoscalerPureTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = os.path.join(self.tmp.name, "a.db")
self.conn = _sqlite_db(self.db, workers=True)
def tearDown(self):
self.conn.close()
self.tmp.cleanup()
def _add_task(self, tid, status, deps=None, executor=None,
parts=None, hb=None):
self.conn.execute(
"INSERT INTO tasks VALUES(?, ?, ?, ?, ?, ?)",
(tid, status,
json.dumps(deps) if deps is not None else None,
executor, parts, hb))
self.conn.commit()
def test_queue_depth_variants(self):
self._add_task("a", "queued")
self._add_task("b", "queued", deps=["a"])
self._add_task("c", "completed")
self._add_task("d", "queued", deps=["c"])
self._add_task("e", "queued", deps=None)
self.assertEqual(autoscaler_mod.queue_depth(self.db), 3)
# 坏 JSON 依赖当无依赖。
self.conn.execute(
"INSERT INTO tasks VALUES('f', 'queued', 'not-json',"
" NULL, NULL, NULL)")
self.conn.commit()
self.assertEqual(autoscaler_mod.queue_depth(self.db), 4)
self._add_task("i", "queued", executor="docker")
self.assertEqual(autoscaler_mod.queue_depth(
self.db, executor="local"), 4)
self.assertEqual(autoscaler_mod.queue_depth(
self.db, executor="docker"), 5)
# 成环任务永不就绪(依赖不在 completed 集里),但不影响
# 其他就绪任务的计数:仍为 a、d、e、f、i 共 5。
self.conn.execute(
"INSERT INTO tasks VALUES('g', 'queued', '[\"h\"]',"
" NULL, NULL, NULL)")
self.conn.execute(
"INSERT INTO tasks VALUES('h', 'queued', '[\"g\"]',"
" NULL, NULL, NULL)")
self.conn.commit()
self.assertEqual(autoscaler_mod.queue_depth(self.db), 5)
def test_desired_workers(self):
f = autoscaler_mod.desired_workers
self.assertEqual(f(0, 2, 1, 3), 1)
self.assertEqual(f(10, 0, 1, 3), 3)
self.assertEqual(f(10, 3, 1, 2), 2)
self.assertEqual(f(5, 2, 1, 10), 3)
self.assertEqual(f(-1, 2, 2, 5), 2)
def test_live_heartbeat(self):
f = autoscaler_mod._is_live_heartbeat
now = time.time()
self.assertFalse(f(None, now, 120))
self.assertFalse(f("bogus", now, 120))
self.assertTrue(f(now - 10, now, 120))
self.assertTrue(f(now + 60, now, 120))
self.assertFalse(f(now - 1000, now, 120))
def test_running_task_helpers(self):
now = time.time()
self._add_task("r1", "running",
parts='[{"name": "a", "prompt": "do A"}]',
hb=now - 5)
self._add_task("r2", "running", parts="bad-json",
hb=now - 5)
self._add_task("r3", "running",
parts='[{"name": "a", "prompt": "do A"}]',
hb=now - 10000)
self.assertTrue(autoscaler_mod._has_live_running_coordinator(
self.db, now=now))
self.assertTrue(autoscaler_mod._has_live_running_task(
self.db, now=now))
# 全部心跳陈旧时两者皆 False。
self.conn.execute("UPDATE tasks SET last_heartbeat = ?",
(now - 10000,))
self.conn.commit()
self.assertFalse(autoscaler_mod._has_live_running_coordinator(
self.db, now=now))
self.assertFalse(autoscaler_mod._has_live_running_task(
self.db, now=now))
def test_worker_registry_running(self):
now = time.time()
f = autoscaler_mod._worker_registry_running
self.assertEqual(f(self.db, "nope", now=now), 0)
self.conn.execute(
"INSERT INTO workers VALUES('w1', 'docker', 2, 3, ?, ?)",
(now - 5, now - 5))
self.conn.execute(
"INSERT INTO workers VALUES('w2', 'docker', 2, 9, ?, ?)",
(now - 10000, now - 10000))
self.conn.execute(
"INSERT INTO workers VALUES('w3', 'docker', 2, NULL, ?, ?)",
(now - 5, now - 5))
self.conn.commit()
self.assertEqual(f(self.db, "w1", now=now), 3)
self.assertEqual(f(self.db, "w2", now=now), 0)
self.assertEqual(f(self.db, "w3", now=now), 0)
# workers 表缺失时按 0 处理。
os.remove(self.db)
self.assertEqual(f(self.db, "w1", now=now), 0)
def test_victim_is_busy(self):
now = time.time()
self.conn.execute(
"INSERT INTO workers VALUES('w1', 'docker', 2, 1, ?, ?)",
(now - 5, now - 5))
self.conn.commit()
# 注册表报 busy 但 DB 无活任务:幽灵占位,不拦。
self.assertFalse(autoscaler_mod._victim_is_busy(
self.db, "w1", now=now))
self._add_task("r1", "running", hb=now - 5)
self.assertTrue(autoscaler_mod._victim_is_busy(
self.db, "w1", now=now))
# 注册表报 0:直接不忙。
self.assertFalse(autoscaler_mod._victim_is_busy(
self.db, "nobody", now=now))
class _FakeProc:
def __init__(self, pid=4242, poll_result=None, wait_raises=False):
self.pid = pid
self._poll = poll_result
self.returncode = poll_result
self.signals = []
self.wait_calls = 0
self.wait_raises = wait_raises
def poll(self):
return self._poll
def send_signal(self, sig):
self.signals.append(sig)
def wait(self, timeout=None):
self.wait_calls += 1
if self.wait_raises and self.wait_calls == 1:
import subprocess as _sp
raise _sp.TimeoutExpired("cmd", timeout)
self._poll = 0
self.returncode = 0
return 0
class AutoscalerProcTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = os.path.join(self.tmp.name, "a.db")
self.state = os.path.join(self.tmp.name, "state.json")
self.log_dir = os.path.join(self.tmp.name, "logs")
def tearDown(self):
self.tmp.cleanup()
def _scaler(self, **kw):
kw.setdefault("worker_args", [])
kw.setdefault("state_file", self.state)
kw.setdefault("log_dir", self.log_dir)
return autoscaler_mod.Autoscaler(self.db, **kw)
def test_worker_command_variants(self):
s = self._scaler(worker_cmd=["/bin/worker", "--x"])
self.assertEqual(s._worker_command("w1"),
["/bin/worker", "--x"])
s = self._scaler(worker_args=["--loop", "--db", "/tmp/d.db",
"--host-name", "h1"])
cmd = s._worker_command("w9")
self.assertIn("--worker-id", cmd)
self.assertEqual(cmd.count("--loop"), 1)
self.assertEqual(cmd.count("--db"), 1)
self.assertEqual(cmd.count("--host-name"), 1)
s = self._scaler(worker_args=[])
cmd = s._worker_command("w1")
self.assertIn("--loop", cmd)
self.assertIn("--db", cmd)
self.assertIn("--host-name", cmd)
def test_start_stop_reap_next_id(self):
s = self._scaler()
procs = []
with mock.patch.object(autoscaler_mod.subprocess, "Popen",
side_effect=lambda *a, **k: procs.pop(0)):
procs.append(_FakeProc(pid=111))
p = s._start_worker("autoscale-1")
self.assertEqual(p.pid, 111)
self.assertTrue(os.path.exists(
os.path.join(self.log_dir, "autoscale-1.log")))
self.assertEqual(s._next_worker_id(), "autoscale-2")
# 停不存在的 worker 返回 True。
self.assertTrue(s._stop_worker("ghost"))
# 超时路径:wait 先 TimeoutExpired,再 killpg + 等到退出。
s.workers["autoscale-1"]["proc"] = _FakeProc(
pid=111, wait_raises=True)
with mock.patch.object(autoscaler_mod.os, "killpg"):
self.assertTrue(s._stop_worker("autoscale-1"))
self.assertNotIn("autoscale-1", s.workers)
# reap:已死 proc 被回收、日志句柄关闭。
dead = _FakeProc(pid=222, poll_result=1)
fh = open(os.path.join(self.tmp.name, "x.log"), "wb")
s.workers["autoscale-2"] = {"proc": dead, "started_at": 1.0,
"log": fh}
s._reap()
self.assertNotIn("autoscale-2", s.workers)
self.assertTrue(fh.closed)
def test_write_state_and_run_loop_stop(self):
s = self._scaler()
s.workers["autoscale-1"] = {
"proc": _FakeProc(pid=777), "started_at": 123.0,
"log": None}
s.last_depth = 4
s.last_desired = 2
s._write_state()
with open(self.state) as f:
state = json.load(f)
self.assertEqual(state["actual"], 1)
self.assertEqual(state["workers"][0]["pid"], 777)
self.assertEqual(state["depth"], 4)
# 无 state_file 时 _write_state 是空操作。
s2 = autoscaler_mod.Autoscaler(self.db, [])
s2._write_state()
# run_loop:stop_event 已置位则立即退出;finally 停掉
# 全部 worker 后再写 state,actual 回到 0。
import threading
ev = threading.Event()
ev.set()
s.run_loop(stop_event=ev)
with open(self.state) as f:
state = json.load(f)
self.assertEqual(state["actual"], 0)
s.request_stop()
self.assertTrue(s._stop)
# ---------------------------------------------------------- playwright
class _SimpleQueue:
"""queue.Queue 的最小替身(只给 screencast sink 用)。
刻意不用 stdlib queue:cloud/tests/test_queue_sandbox.py
靠 cloud/queue.py 遮蔽 stdlib queue(它把 cloud/ 插进
sys.path 再 from queue import);discover 按字母顺序先
导入本文件时若已缓存 stdlib queue,那个模块就 ImportError。
本步实测:import queue 一行导致 cloud 套件 47 项未跑。
"""
def __init__(self):
self._items = []
def put(self, item):
self._items.append(item)
def get_nowait(self):
if not self._items:
raise RuntimeError("empty")
return self._items.pop(0)
class _FakeMouse:
def __init__(self):
self.moves = []
self.clicks = []
def move(self, x, y):
self.moves.append((x, y))
def click(self, x, y):
self.clicks.append((x, y))
class _FakeKeyboard:
def __init__(self):
self.pressed = []
self.typed = []
def press(self, key):
self.pressed.append(key)
def type(self, text):
self.typed.append(text)
class _FakeLocator:
def __init__(self, page, selector):
self.page = page
self.selector = selector
self.clicked = False
def inner_text(self, timeout=None):
return self.page.body_text
@property
def first(self):
return self
def click(self, timeout=None):
self.clicked = True
self.page.locator_clicks.append(self.selector)
class _FakeCDP:
def __init__(self):
self.handlers = {}
self.sent = []
def on(self, event, handler):
self.handlers[event] = handler
def send(self, method, params=None):
self.sent.append((method, params))
if method == "Page.captureScreenshot":
return {"data": "eager-b64"}
return {}
class _FakeContext:
def __init__(self, page):
self.page = page
self.cdp = None
def new_cdp_session(self, page):
self.cdp = _FakeCDP()
return self.cdp
class _FakePage:
def __init__(self):
self.url = "about:blank"
self._title = "Fake Title"
self.body_text = "hello world"
self.mouse = _FakeMouse()
self.keyboard = _FakeKeyboard()
self.locator_clicks = []
self.context = _FakeContext(self)
self.shots = []
self.goto_fail = False
self.eval_result = None
def title(self):
return self._title
def goto(self, url, timeout=None, wait_until=None):
if self.goto_fail:
raise RuntimeError("net fail")
self.url = url
def click(self, selector, timeout=None):
self.locator_clicks.append(selector)
def fill(self, selector, text, timeout=None):
self.filled = (selector, text)
def locator(self, selector):
return _FakeLocator(self, selector)
def evaluate(self, script):
if self.eval_result is not None:
return self.eval_result
return "eval-ok"
def screenshot(self, path=None):
with open(path, "wb") as f:
f.write(b"PNGDATA")
self.shots.append(path)
def close(self):
self.closed = True
class _FakeBrowser:
def __init__(self, page):
self.page = page
self.closed = False
def new_page(self, viewport=None):
return self.page
def close(self):
self.closed = True
class _FakePW:
def __init__(self, page, fail_launch=False):
self.page = page
self.fail_launch = fail_launch
self.stopped = False
self.launch_kwargs = None
class _Chromium:
def __init__(self, outer):
self.outer = outer
def launch(self, **kw):
self.outer.launch_kwargs = kw
if self.outer.fail_launch:
raise RuntimeError("no chromium")
return _FakeBrowser(self.outer.page)
self.chromium = _Chromium(self)
def start(self):
return self
def stop(self):
self.stopped = True
class PlaywrightBrowserTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.page = _FakePage()
self.env = mock.patch.dict(os.environ, {"HTTPS_PROXY": "",
"HTTP_PROXY": ""})
self.env.start()
def tearDown(self):
self.env.stop()
self.tmp.cleanup()
def _browser(self, proxy=None, fail_launch=False):
return pw_mod.Browser(
cwd=self.tmp.name, proxy=proxy,
browser_factory=lambda: _FakePW(self.page, fail_launch))
def test_navigate_and_info(self):
b = self._browser(proxy="http://proxy:1")
out = b.navigate("https://example.com")
self.assertTrue(out["ok"])
self.assertEqual(out["url"], "https://example.com")
self.assertEqual(out["title"], "Fake Title")
self.assertEqual(b.cached_info()["url"], "https://example.com")
self.assertTrue(b.cached_info()["started"])
with self.assertRaises(pw_mod.BrowserError):
b.navigate(" ")
self.page.goto_fail = True
with self.assertRaises(pw_mod.BrowserError):
b.navigate("https://example.com")
def test_launch_failure(self):
b = self._browser(fail_launch=True)
with self.assertRaises(pw_mod.BrowserError) as cm:
b.snapshot()
self.assertIn("browser launch failed", str(cm.exception))
def test_snapshot_truncation_and_failure(self):
b = self._browser()
out = b.snapshot()
self.assertEqual(out["text"], "hello world")
self.assertFalse(out["truncated"])
self.page.body_text = "x" * 9000
out = b.snapshot()
self.assertTrue(out["truncated"])
self.assertEqual(len(out["text"]), 8000)
orig = self.page.locator
self.page.locator = lambda s: (_ for _ in ()).throw(
RuntimeError("loc fail"))
with self.assertRaises(pw_mod.BrowserError):
b.snapshot()
self.page.locator = orig
def test_click_fill_key_type(self):
b = self._browser()
self.assertTrue(b.click("button.ok")["ok"])
self.assertIn("button.ok", self.page.locator_clicks)
with self.assertRaises(pw_mod.BrowserError):
b.click("")
self.assertTrue(b.fill("#q", None)["ok"])
self.assertEqual(self.page.filled, ("#q", ""))
with self.assertRaises(pw_mod.BrowserError):
b.fill("", "x")
self.assertTrue(b.key_press("Enter")["ok"])
self.assertEqual(self.page.keyboard.pressed, ["Enter"])
with self.assertRaises(pw_mod.BrowserError):
b.key_press(" ")
self.assertTrue(b.type_text("hi")["ok"])
self.assertEqual(self.page.keyboard.typed, ["hi"])
with self.assertRaises(pw_mod.BrowserError):
b.type_text(None)
def test_mouse_and_click_at(self):
b = self._browser()
out = b.mouse_move(10, 20)
self.assertEqual((out["x"], out["y"]), (10.0, 20.0))
self.assertEqual(self.page.mouse.moves, [(10.0, 20.0)])
with self.assertRaises(pw_mod.BrowserError):
b.mouse_move("a", 1)
with self.assertRaises(pw_mod.BrowserError):
b.mouse_move(2000, 10)
out = b.click_at(5, 6)
self.assertTrue(out["ok"])
self.assertEqual(self.page.mouse.clicks, [(5.0, 6.0)])
with self.assertRaises(pw_mod.BrowserError):
b.click_at("x", 1)
with self.assertRaises(pw_mod.BrowserError):
b.click_at(10, 900)
def test_snapshot_refs_and_click_ref(self):
b = self._browser()
with self.assertRaises(pw_mod.BrowserError):
b.click_ref("e0")
self.page.eval_result = [
{"tag": "button", "role": "", "name": "OK",
"path": "body > button"},
{"tag": "a", "role": "link", "name": "Home",
"path": "body > a"}]
out = b.snapshot_refs()
self.assertEqual(out["count"], 2)
self.assertEqual(out["elements"][0]["ref"], "e0")
out = b.click_ref("e1")
self.assertTrue(out["ok"])
self.assertEqual(out["ref"], "e1")
self.assertIn("body > a", self.page.locator_clicks)
def test_screencast_sink_and_pump(self):
b = self._browser()
self.assertEqual(b.pump_screencast(), "")
sink = _SimpleQueue()
b.set_frame_sink(sink)
b.navigate("https://example.com")
# navigate 后 sink 已接上 CDP(_start_screencast 在
# navigate 内跑);再显式 set 一次走“已接同一页”短路。
b.set_frame_sink(sink)
cdp = self.page.context.cdp
self.assertIsNotNone(cdp)
cdp.handlers["Page.screencastFrame"](
{"sessionId": "s1", "data": "frame-b64"})
self.assertEqual(b.pump_screencast(), "frame-b64")
self.assertIn(("Page.screencastFrameAck",
{"sessionId": "s1"}), cdp.sent)
# eager 帧(空 session)只存不 ack。
sink.put(("", "eager-b64"))
self.assertEqual(b.pump_screencast(), "eager-b64")
self.assertEqual(
[s for s in cdp.sent if s[0] == "Page.screencastFrameAck"],
[("Page.screencastFrameAck", {"sessionId": "s1"})])
def test_live_shot_screenshot_evaluate_close(self):
b = self._browser()
status = b.live_shot()
self.assertEqual(status["image"], "browser-live.png")
self.assertEqual(status["bytes"], 7)
with open(status["status_path"]) as f:
sidecar = json.load(f)
self.assertEqual(sidecar["image"], "browser-live.png")
shot = b.screenshot()
self.assertTrue(shot["filename"].startswith("shot-"))
self.assertEqual(shot["bytes"], 7)
shot2 = b.screenshot("my.png")
self.assertEqual(shot2["filename"], "my.png")
with self.assertRaises(pw_mod.BrowserError):
b.screenshot("../escape.png")
self.assertEqual(b.evaluate("1+1")["result"], "eval-ok")
with self.assertRaises(pw_mod.BrowserError):
b.evaluate(" ")
out = b.close()
self.assertTrue(out["closed"])
self.assertEqual(out["errors"], [])
# 已关闭后再次 close 仍返回 closed(各 closer 遇 None 短路)。
out = b.close()
self.assertTrue(out["closed"])
class PlaywrightEnsureTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.src = os.path.join(self.tmp.name, "src")
self.home = os.path.join(self.tmp.name, "home")
os.makedirs(self.src)
for name in ("playwright_browser.py", "playwright_mcp.py",
"playwright_remote.py"):
with open(os.path.join(self.src, name), "w") as f:
f.write("# %s v1\n" % name)
def tearDown(self):
self.tmp.cleanup()
def test_seed_refresh_and_config(self):
out = pw_mod.ensure_codex_home_playwright(
self.home, script_dir=self.src)
self.assertTrue(out["script_changed"])
self.assertTrue(out["config_changed"])
with open(os.path.join(self.home, "config.toml")) as f:
cfg = f.read()
self.assertIn("[mcp_servers.playwright]", cfg)
# 第二次:脚本相同、配置已有 section,全部不变。
out = pw_mod.ensure_codex_home_playwright(
self.home, script_dir=self.src)
self.assertFalse(out["script_changed"])
self.assertFalse(out["config_changed"])
# 上游改版刷新:源脚本升级(v1→v2)后,manifest 已证明
# 家目录旧脚本未改动,自动刷新并上报。
with open(os.path.join(self.src,
"playwright_mcp.py"), "w") as f:
f.write("# playwright_mcp.py v2\n")
out = pw_mod.ensure_codex_home_playwright(
self.home, script_dir=self.src)
self.assertIn("playwright_mcp.py", out["script_refreshed"])
# 未知改动不刷新:家目录脚本被改成 manifest 与已知
# 已发版哈希都不认识的内容时,只上报跳过、绝不覆盖。
with open(os.path.join(self.home,
"playwright_mcp.py"), "w") as f:
f.write("# stale\n")
out = pw_mod.ensure_codex_home_playwright(
self.home, script_dir=self.src)
self.assertNotIn("playwright_mcp.py", out["script_refreshed"])
self.assertIn("playwright_mcp.py", out["script_skipped_modified"])
# 自定义 command_args 写进 section(删旧配置重写,
# 否则已有 section 不会被改写)。
os.remove(os.path.join(self.home, "config.toml"))
out = pw_mod.ensure_codex_home_playwright(
self.home, command_args=["py3", "mcp.py", "--x"],
script_dir=self.src)
self.assertTrue(out["config_changed"])
with open(os.path.join(self.home, "config.toml")) as f:
cfg = f.read()
self.assertIn('"py3"', cfg)
def test_config_no_trailing_newline(self):
os.makedirs(self.home)
with open(os.path.join(self.home, "config.toml"), "w") as f:
f.write("[other]")
pw_mod.ensure_codex_home_playwright(self.home,
script_dir=self.src)
with open(os.path.join(self.home, "config.toml")) as f:
cfg = f.read()
self.assertIn("[other]\n\n[mcp_servers.playwright]", cfg)
def test_missing_src_script_skipped(self):
os.remove(os.path.join(self.src, "playwright_remote.py"))
out = pw_mod.ensure_codex_home_playwright(
self.home, script_dir=self.src)
self.assertTrue(out["script_changed"])
self.assertFalse(os.path.exists(
os.path.join(self.home, "playwright_remote.py")))
def test_mcp_env_writer(self):
os.makedirs(self.home)
self.assertFalse(pw_mod.ensure_playwright_mcp_env(
self.home, {}))
self.assertTrue(pw_mod.ensure_playwright_mcp_env(
self.home, {"B": "2", "A": "1"}))
with open(os.path.join(self.home, "config.toml")) as f:
cfg = f.read()
self.assertIn("[mcp_servers.playwright.env]", cfg)
self.assertLess(cfg.index('A = '), cfg.index('B = '))
# 幂等替换:旧 env 段被新段取代,不残留旧 key。
self.assertTrue(pw_mod.ensure_playwright_mcp_env(
self.home, {"C": "3"}))
with open(os.path.join(self.home, "config.toml")) as f:
cfg = f.read()
self.assertIn('C = ', cfg)
self.assertNotIn('A = ', cfg)
if __name__ == "__main__":
unittest.main()