Integrate per-user browser runtimes into subagent API
This commit is contained in:
parent
952b2e7d17
commit
280247e1e5
11 changed files with 777 additions and 21 deletions
97
api/tests/test_browser_runtime_manager.py
Normal file
97
api/tests/test_browser_runtime_manager.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def test_resolve_isolation_owner_prefers_user_id():
|
||||
from api.services.browser_runtime_manager import resolve_isolation_owner
|
||||
|
||||
owner = resolve_isolation_owner(
|
||||
"docker-per-principal",
|
||||
task_id="task-1",
|
||||
metadata={"user_id": "user-7", "session_id": "session-9"},
|
||||
thread_id="thread-1",
|
||||
)
|
||||
|
||||
assert owner == "user-7"
|
||||
|
||||
|
||||
def test_resolve_isolation_owner_uses_task_for_per_task_mode():
|
||||
from api.services.browser_runtime_manager import resolve_isolation_owner
|
||||
|
||||
owner = resolve_isolation_owner(
|
||||
"docker-per-task",
|
||||
task_id="task-42",
|
||||
metadata={"user_id": "user-7"},
|
||||
thread_id="thread-1",
|
||||
)
|
||||
|
||||
assert owner == "task-42"
|
||||
|
||||
|
||||
def test_hash_runtime_owner_is_stable():
|
||||
from api.services.browser_runtime_manager import hash_runtime_owner
|
||||
|
||||
assert hash_runtime_owner("owner-1") == hash_runtime_owner("owner-1")
|
||||
assert hash_runtime_owner("owner-1") != hash_runtime_owner("owner-2")
|
||||
|
||||
|
||||
def test_shared_mode_returns_shared_runtime(monkeypatch):
|
||||
from api.services import browser_runtime_manager
|
||||
|
||||
monkeypatch.setenv("BROWSER_USE_ISOLATION_MODE", "shared")
|
||||
monkeypatch.setenv("BROWSER_URL", "http://shared-browser:9333")
|
||||
monkeypatch.setenv("BROWSER_USE_RPC_URL", "http://shared-browser:8787/run")
|
||||
monkeypatch.setenv("BROWSER_VIEW_BASE_URL", "https://viewer.example.com")
|
||||
|
||||
runtime = browser_runtime_manager.ensure_browser_runtime(
|
||||
task_id="task-1",
|
||||
metadata={"user_id": "user-7"},
|
||||
thread_id="thread-1",
|
||||
)
|
||||
|
||||
assert runtime["cdp_url"] == "http://shared-browser:9333"
|
||||
assert runtime["rpc_url"] == "http://shared-browser:8787/run"
|
||||
assert runtime["browser_view"] == "https://viewer.example.com/vnc.html?path=websockify"
|
||||
assert runtime["isolation_mode"] == "shared"
|
||||
|
||||
|
||||
def test_isolated_mode_starts_container(monkeypatch):
|
||||
from api.services import browser_runtime_manager
|
||||
|
||||
monkeypatch.setenv("BROWSER_USE_ISOLATION_MODE", "docker-per-principal")
|
||||
monkeypatch.setenv("BROWSER_RUNTIME_IMAGE", "browser-use-browser-runtime:test")
|
||||
monkeypatch.setenv("BROWSER_RUNTIME_NETWORK", "browser-net")
|
||||
monkeypatch.setenv("BROWSER_VIEW_BASE_URL", "https://viewer.example.com")
|
||||
|
||||
saved_registry = {}
|
||||
docker_calls = []
|
||||
|
||||
def fake_run_docker(args, check=True):
|
||||
docker_calls.append(args)
|
||||
if args[:2] == ["inspect", "-f"]:
|
||||
return MagicMock(returncode=1, stdout="", stderr="")
|
||||
if args[:1] == ["inspect"]:
|
||||
return MagicMock(returncode=1, stdout="", stderr="")
|
||||
return MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
|
||||
with (
|
||||
patch.object(browser_runtime_manager, "_load_registry", return_value={"runtimes": {}}),
|
||||
patch.object(browser_runtime_manager, "_save_registry", side_effect=lambda payload: saved_registry.update(payload)),
|
||||
patch.object(browser_runtime_manager, "_run_docker", side_effect=fake_run_docker),
|
||||
patch.object(browser_runtime_manager, "_wait_for_runtime") as mock_wait,
|
||||
):
|
||||
runtime = browser_runtime_manager.ensure_browser_runtime(
|
||||
task_id="task-1",
|
||||
metadata={"user_id": "user-7"},
|
||||
thread_id="thread-1",
|
||||
)
|
||||
|
||||
assert runtime["isolation_mode"] == "docker-per-principal"
|
||||
assert runtime["cdp_url"].startswith("http://browser-use-browser-")
|
||||
assert runtime["rpc_url"].startswith("http://browser-use-browser-")
|
||||
assert runtime["rpc_url"].endswith(":8787/run")
|
||||
assert "/view/" in runtime["browser_view"]
|
||||
assert saved_registry["runtimes"]
|
||||
run_commands = [call for call in docker_calls if call[:2] == ["run", "-d"]]
|
||||
assert run_commands
|
||||
assert "browser-use-browser-runtime:test" in run_commands[0]
|
||||
mock_wait.assert_called_once()
|
||||
62
api/tests/test_task_service_browser_runtime.py
Normal file
62
api/tests/test_task_service_browser_runtime.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
|
||||
class FakeRpcClient:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
async def run(self, task: str, timeout_sec: float, rpc_url: str | None = None) -> dict[str, Any]:
|
||||
self.calls.append({"task": task, "timeout_sec": timeout_sec, "rpc_url": rpc_url})
|
||||
return {"success": True, "result": "done"}
|
||||
|
||||
|
||||
def test_task_service_routes_run_to_browser_runtime(monkeypatch):
|
||||
from api.repositories.task_store import TaskStore
|
||||
from api.services import task_service as task_service_module
|
||||
from api.services.task_service import TaskService
|
||||
|
||||
runtime = {
|
||||
"rpc_url": "http://browser-use-browser-abc:8787/run",
|
||||
"browser_view": "https://viewer.example.com/view/abc/vnc.html?path=view/abc/websockify",
|
||||
"isolation_mode": "docker-per-principal",
|
||||
"owner_hash": "abc",
|
||||
}
|
||||
cleanup_calls = []
|
||||
|
||||
monkeypatch.setattr(task_service_module, "ensure_browser_runtime", lambda **_: runtime)
|
||||
monkeypatch.setattr(task_service_module, "cleanup_browser_runtime", lambda **kwargs: cleanup_calls.append(kwargs))
|
||||
|
||||
async def scenario():
|
||||
rpc_client = FakeRpcClient()
|
||||
service = TaskService(
|
||||
store=TaskStore(),
|
||||
rpc_client=rpc_client,
|
||||
max_concurrency=1,
|
||||
rpc_timeout_cap=30,
|
||||
)
|
||||
rec = await service.create_run(
|
||||
thread_id="thread-1",
|
||||
user_input="open example.com",
|
||||
timeout=60,
|
||||
metadata={"user_id": "user-7"},
|
||||
)
|
||||
done = await service.wait_run(rec.task_id, timeout=2)
|
||||
await service.close()
|
||||
return rpc_client, done
|
||||
|
||||
rpc_client, done = asyncio.run(scenario())
|
||||
|
||||
assert rpc_client.calls == [
|
||||
{
|
||||
"task": "open example.com",
|
||||
"timeout_sec": 30,
|
||||
"rpc_url": "http://browser-use-browser-abc:8787/run",
|
||||
}
|
||||
]
|
||||
assert done is not None
|
||||
assert done.raw_response is not None
|
||||
assert done.raw_response["browser_view"] == runtime["browser_view"]
|
||||
assert done.raw_response["isolation_mode"] == "docker-per-principal"
|
||||
assert done.raw_response["owner_hash"] == "abc"
|
||||
assert cleanup_calls
|
||||
Loading…
Add table
Add a link
Reference in a new issue