diff --git a/chv-guest/chv-browser.py b/chv-guest/chv-browser.py index a1f3804..483ae80 100644 --- a/chv-guest/chv-browser.py +++ b/chv-guest/chv-browser.py @@ -104,7 +104,12 @@ def truncate_a11y(node, max_nodes=MAX_A11Y_NODES, 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", PLAYWRIGHT_BROWSERS_PATH) try: @@ -113,7 +118,7 @@ def _load_playwright(): raise RuntimeError( "chv-browser: playwright 未装(%s),浏览器子代理不可用" % e) - return sync_playwright + return sync_playwright() class BrowserAgent: @@ -208,10 +213,17 @@ FORBIDDEN_OPS = ("evaluate", "content", "devtools", "js") 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): return {"ok": False, "error": "请求须是 JSON 对象"} 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: return {"ok": False, "error": "操作 %r 不暴露(无 JS/无原始 DOM)" % op} diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index 033274e..5c2cd60 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -45,10 +45,13 @@ class CellArgvTest(unittest.TestCase): self.assertIn("--as-pid2", argv) # round-11 真机修复:nspawn 255 拒裸 --no-new-privileges #(当作带必填参数的选项)与裸 --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.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): argv = CELL.build_nspawn_argv(["true"]) @@ -411,6 +414,65 @@ class BrowserTest(unittest.TestCase): self.assertTrue(os.path.exists(shot["path"])) 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: def __init__(self):