fix(chv-muse-r11fix): chv-browser 补 sync_playwright 调用括号 + 接受 broker 的 browser op(复测新发现)
复测(RETEST.md §browser-defect、§broker-proto):
1. _load_playwright 返回裸 sync_playwright 函数(未调用),
_ensure_browser 对它调 __enter__ 即 AttributeError,open
永远失败。改返回 sync_playwright()(调用后的上下文管理器),
与单测假工厂的形状一致。回归测试用 stub 的 playwright 模块
钉住“返回调用结果、不是函数本身”。
2. broker 按设计原样转发 {op: browser, …},chv-browser 白名单
却不认 browser。在 chv-browser 的 handle_request 里归一化:
内层 op 在 action 里;无 action 但带 url 时默认 open(驱动
只发 url);action 同样受 FORBIDDEN_OPS 约束。broker 保持
原样转发不动(其 docstring 与单测均以此为契约)。
附:本提交含 test_chv_muse_p2.py 里 CellArgvTest 的配套更新
(c1d00b3 的 --volatile=yes 断言,同文件无法拆分提交)。
This commit is contained in:
1 parent
c1d00b3620
commit
04287f6053
2 files changed
+79
-5
No files matched your search
@@ -104,7 +104,12 @@ def truncate_a11y(node, max_nodes=MAX_A11Y_NODES,
|
|||||||
|
|
||||||
|
|
||||||
def _load_playwright():
|
def _load_playwright():
|
||||||
"""懒加载 playwright;缺失时报明错(不静默假装有浏览器)。"""
|
"""懒加载 playwright;缺失时报明错(不静默假装有浏览器)。
|
||||||
|
返回调用后的上下文管理器(sync_playwright()),不是函数本身——
|
||||||
|
round-11 复测回归:曾返回裸 sync_playwright 函数,
|
||||||
|
_ensure_browser 对它调 __enter__ 即 AttributeError,open
|
||||||
|
永远失败;单测的假工厂直接返回带 __enter__ 的对象,盖不住
|
||||||
|
这个形状(见 test_load_playwright_returns_called_cm)。"""
|
||||||
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH",
|
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH",
|
||||||
PLAYWRIGHT_BROWSERS_PATH)
|
PLAYWRIGHT_BROWSERS_PATH)
|
||||||
try:
|
try:
|
||||||
@@ -113,7 +118,7 @@ def _load_playwright():
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"chv-browser: playwright 未装(%s),浏览器子代理不可用"
|
"chv-browser: playwright 未装(%s),浏览器子代理不可用"
|
||||||
% e)
|
% e)
|
||||||
return sync_playwright
|
return sync_playwright()
|
||||||
|
|
||||||
|
|
||||||
class BrowserAgent:
|
class BrowserAgent:
|
||||||
@@ -208,10 +213,17 @@ FORBIDDEN_OPS = ("evaluate", "content", "devtools", "js")
|
|||||||
|
|
||||||
|
|
||||||
def handle_request(agent, req):
|
def handle_request(agent, req):
|
||||||
"""处理一个请求 dict,返回响应 dict(纯逻辑,可单测)。"""
|
"""处理一个请求 dict,返回响应 dict(纯逻辑,可单测)。
|
||||||
|
op "browser" 是 broker 原样转发的形状(round-11 真机修复):
|
||||||
|
内层 op 在 action 里;无 action 但带 url 时默认 open(驱动
|
||||||
|
只发 url);action 同样受 FORBIDDEN_OPS 约束。"""
|
||||||
if not isinstance(req, dict):
|
if not isinstance(req, dict):
|
||||||
return {"ok": False, "error": "请求须是 JSON 对象"}
|
return {"ok": False, "error": "请求须是 JSON 对象"}
|
||||||
op = req.get("op")
|
op = req.get("op")
|
||||||
|
if op == "browser":
|
||||||
|
op = req.get("action")
|
||||||
|
if op is None and req.get("url"):
|
||||||
|
op = "open"
|
||||||
if op in FORBIDDEN_OPS:
|
if op in FORBIDDEN_OPS:
|
||||||
return {"ok": False,
|
return {"ok": False,
|
||||||
"error": "操作 %r 不暴露(无 JS/无原始 DOM)" % op}
|
"error": "操作 %r 不暴露(无 JS/无原始 DOM)" % op}
|
||||||
|
|||||||
@@ -45,10 +45,13 @@ class CellArgvTest(unittest.TestCase):
|
|||||||
self.assertIn("--as-pid2", argv)
|
self.assertIn("--as-pid2", argv)
|
||||||
# round-11 真机修复:nspawn 255 拒裸 --no-new-privileges
|
# round-11 真机修复:nspawn 255 拒裸 --no-new-privileges
|
||||||
#(当作带必填参数的选项)与裸 --directory=/(须
|
#(当作带必填参数的选项)与裸 --directory=/(须
|
||||||
# --ephemeral/--volatile),见 p2.log 的解析报错。
|
# --volatile/--ephemeral)。复测纠正:--ephemeral 在客内
|
||||||
|
# ext4 根盘上做整盘快照拷贝,2.4G 盘只剩 593M 即 ENOSPC,
|
||||||
|
# 改 --volatile=yes(tmpfs 根 + 只读 /usr,不拷盘)。
|
||||||
self.assertIn("--no-new-privileges=yes", argv)
|
self.assertIn("--no-new-privileges=yes", argv)
|
||||||
self.assertNotIn("--no-new-privileges", argv)
|
self.assertNotIn("--no-new-privileges", argv)
|
||||||
self.assertIn("--ephemeral", argv)
|
self.assertIn("--volatile=yes", argv)
|
||||||
|
self.assertNotIn("--ephemeral", argv)
|
||||||
|
|
||||||
def test_muse_named_capabilities_dropped(self):
|
def test_muse_named_capabilities_dropped(self):
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
@@ -411,6 +414,65 @@ class BrowserTest(unittest.TestCase):
|
|||||||
self.assertTrue(os.path.exists(shot["path"]))
|
self.assertTrue(os.path.exists(shot["path"]))
|
||||||
self.assertTrue(agent.close()["ok"])
|
self.assertTrue(agent.close()["ok"])
|
||||||
|
|
||||||
|
def test_load_playwright_returns_called_cm(self):
|
||||||
|
# round-11 复测回归:真实 _load_playwright 曾返回裸
|
||||||
|
# sync_playwright 函数(未调用),_ensure_browser 对它调
|
||||||
|
# __enter__ 即 AttributeError,open 永远失败;单测的假工厂
|
||||||
|
# 直接返回带 __enter__ 的对象,盖不住这个形状。现
|
||||||
|
# _load_playwright 必须返回调用后的上下文管理器。
|
||||||
|
import types
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class FakeCM:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
return False
|
||||||
|
|
||||||
|
cm = FakeCM()
|
||||||
|
|
||||||
|
def sync_playwright():
|
||||||
|
calls.append("called")
|
||||||
|
return cm
|
||||||
|
|
||||||
|
fake_api = types.ModuleType("playwright.sync_api")
|
||||||
|
fake_api.sync_playwright = sync_playwright
|
||||||
|
fake_pkg = types.ModuleType("playwright")
|
||||||
|
fake_pkg.sync_api = fake_api
|
||||||
|
sys.modules["playwright"] = fake_pkg
|
||||||
|
sys.modules["playwright.sync_api"] = fake_api
|
||||||
|
self.addCleanup(sys.modules.pop, "playwright.sync_api", None)
|
||||||
|
self.addCleanup(sys.modules.pop, "playwright", None)
|
||||||
|
self.assertIs(BROWSER._load_playwright(), cm)
|
||||||
|
self.assertEqual(calls, ["called"])
|
||||||
|
|
||||||
|
def test_browser_op_dispatches_on_action(self):
|
||||||
|
# round-11 复测回归:broker 原样转发 {"op": "browser", …},
|
||||||
|
# chv-browser 白名单却不认 "browser"。内层 op 在 action 里;
|
||||||
|
# 无 action 但带 url 时默认 open(驱动只发 url)。
|
||||||
|
agent = mock.Mock()
|
||||||
|
agent.open.return_value = {"ok": True}
|
||||||
|
resp = BROWSER.handle_request(
|
||||||
|
agent, {"op": "browser", "action": "open",
|
||||||
|
"url": "https://example.com"})
|
||||||
|
self.assertTrue(resp["ok"])
|
||||||
|
agent.open.assert_called_once_with("https://example.com")
|
||||||
|
agent.reset_mock()
|
||||||
|
resp = BROWSER.handle_request(
|
||||||
|
agent, {"op": "browser", "url": "https://example.com"})
|
||||||
|
self.assertTrue(resp["ok"])
|
||||||
|
agent.open.assert_called_once_with("https://example.com")
|
||||||
|
# 内层 forbidden 同样拒绝。
|
||||||
|
agent.reset_mock()
|
||||||
|
resp = BROWSER.handle_request(
|
||||||
|
agent, {"op": "browser", "action": "evaluate"})
|
||||||
|
self.assertFalse(resp["ok"])
|
||||||
|
agent.open.assert_not_called()
|
||||||
|
# 无 action 也无 url:无法推断内层 op。
|
||||||
|
resp = BROWSER.handle_request(agent, {"op": "browser"})
|
||||||
|
self.assertFalse(resp["ok"])
|
||||||
|
|
||||||
|
|
||||||
class FakePage:
|
class FakePage:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
Reference in new issue
Block a user