Files
deerflow-code/offline-backend-20260512/backend/tests/test_browser_act.py
T
AdministratorandClaude Opus 4.8 d4a71ce384 feat: 浏览器扩展支持交互式页面操作 (browser_act)
在原有只读抓取 (browser_fetch_page) 基础上,新增 observe→act 循环,
让 Agent 能在用户的 Chrome 中实际操作网页:点击/输入/滚动/等待/页内跳转。

后端:
- 新工具 browser_act,按 session 复用标签页,返回带编号的可交互元素清单+正文
- browser_context 任务队列扩展 kind/session_id/action 与 observation 结果
- /api/browser-context 路由下发 act 任务、接收 observation
- BrowserContextConfig 增加 actions_enabled 等开关,tools.py 两级门控
- 接线 app.py 路由挂载、app_config.py 配置字段、config.yaml 工具注册

前端:
- ChatPage 处理「发送当前页」回跳的 browser_context query 参数并自动提交

扩展 (browser-extension/deer-web-clipper):
- shared.js 自包含注入函数 deerObservePage / deerPerformAction
- background.js 按 kind 分发任务,session→tab 映射存 chrome.storage.session

测试:tests/test_browser_act.py(队列往返 + 工具校验 + 模拟扩展成功路径,8 passed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:47:00 +08:00

159 lines
5.2 KiB
Python

"""Tests for the interactive browser_act tool and its act-task queue support."""
from __future__ import annotations
import asyncio
import pytest
from deerflow.browser_context import tasks as task_queue
from deerflow.config.browser_context_config import BrowserContextConfig
from deerflow.tools.builtins import browser_act_tool as act_mod
@pytest.fixture(autouse=True)
def _isolated_task_home(tmp_path, monkeypatch):
"""Point the file-backed task queue at a throwaway directory."""
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
yield
def _enable_actions(monkeypatch, *, enabled=True, actions_enabled=True):
cfg = BrowserContextConfig(
enabled=enabled,
actions_enabled=actions_enabled,
action_result_timeout_seconds=5,
task_poll_interval_seconds=0.25,
task_lease_seconds=30,
)
monkeypatch.setattr(act_mod, "_settings", lambda: cfg)
return cfg
# --- task queue layer -------------------------------------------------------
def test_act_task_round_trip_carries_session_and_observation():
task = task_queue.create_browser_task(
user_id="u1",
url="https://example.com",
kind="act",
session_id="sess-1",
action={"type": "navigate", "url": "https://example.com"},
)
assert task["kind"] == "act"
assert task["session_id"] == "sess-1"
claimed = task_queue.claim_next_browser_task(user_id="u1", worker_id="w1")
assert claimed is not None
assert claimed["kind"] == "act"
assert claimed["session_id"] == "sess-1"
assert claimed["action"] == {"type": "navigate", "url": "https://example.com"}
observation = {
"action_status": "ok",
"url": "https://example.com/home",
"title": "Home",
"elements": [{"index": 0, "tag": "button", "label": "Login"}],
"text": "welcome",
}
done = task_queue.complete_browser_task(
task_id=task["id"],
user_id="u1",
observation=observation,
)
assert done is not None
assert done["status"] == "done"
assert done["result"]["observation"] == observation
def test_fetch_task_default_kind_is_fetch():
task = task_queue.create_browser_task(user_id="u1", url="https://example.com")
assert task["kind"] == "fetch"
claimed = task_queue.claim_next_browser_task(user_id="u1", worker_id="w1")
assert claimed["kind"] == "fetch"
assert claimed["session_id"] == ""
assert claimed["action"] is None
# --- tool validation --------------------------------------------------------
async def _invoke(**kwargs) -> str:
return await act_mod.browser_act_tool.ainvoke(kwargs)
@pytest.mark.asyncio
async def test_browser_act_disabled_when_actions_off(monkeypatch):
_enable_actions(monkeypatch, enabled=True, actions_enabled=False)
out = await _invoke(action="navigate", url="https://example.com")
assert "actions_enabled is false" in out
@pytest.mark.asyncio
async def test_browser_act_unknown_action(monkeypatch):
_enable_actions(monkeypatch)
out = await _invoke(action="frobnicate")
assert "unknown action" in out
@pytest.mark.asyncio
async def test_browser_act_navigate_requires_url(monkeypatch):
_enable_actions(monkeypatch)
out = await _invoke(action="navigate")
assert "requires an exact http(s) URL" in out
@pytest.mark.asyncio
async def test_browser_act_non_navigate_needs_session(monkeypatch):
_enable_actions(monkeypatch)
out = await _invoke(action="click", index=0)
assert "no session_id yet" in out
@pytest.mark.asyncio
async def test_browser_act_click_requires_index(monkeypatch):
_enable_actions(monkeypatch)
out = await _invoke(action="click", session_id="sess-1")
assert "requires a non-negative element index" in out
# --- tool success path (simulated extension) --------------------------------
@pytest.mark.asyncio
async def test_browser_act_returns_formatted_observation(monkeypatch):
_enable_actions(monkeypatch)
async def fake_extension():
# Wait for the tool to create the pending act task, then complete it.
for _ in range(40):
claimed = task_queue.claim_next_browser_task(user_id="default", worker_id="ext")
if claimed is not None:
task_queue.complete_browser_task(
task_id=claimed["id"],
user_id="default",
observation={
"action_status": "ok",
"url": "https://example.com/home",
"title": "Home Page",
"elements": [
{"index": 0, "tag": "input", "type": "search", "label": "Search", "value": ""},
{"index": 1, "tag": "button", "label": "Go"},
],
"text": "Welcome to the home page.",
},
)
return
await asyncio.sleep(0.1)
ext = asyncio.create_task(fake_extension())
out = await _invoke(action="navigate", url="https://example.com")
await ext
assert "Browser session:" in out
assert "Home Page" in out
assert "[0] input[search]" in out
assert "[1] button" in out
assert "Welcome to the home page." in out