* fix(tests): mock sys.stdin.isatty for cmd_model TTY guard * fix(tests): update camofox snapshot format + trajectory compressor mock path - test_browser_camofox: mock response now uses snapshot format (accessibility tree) - test_trajectory_compressor: mock _get_async_client instead of setting async_client directly * fix: URL-based auth detection for third-party Anthropic endpoints + test fixes Reverts the key-prefix approach from #4093 which broke JWT and managed key OAuth detection. Instead, detects third-party endpoints by URL: if base_url is set and isn't anthropic.com, it's a proxy (Azure AI Foundry, AWS Bedrock, etc.) that uses x-api-key regardless of key format. Auth decision chain is now: 1. _requires_bearer_auth(url) → MiniMax → Bearer 2. _is_third_party_anthropic_endpoint(url) → Azure/Bedrock → x-api-key 3. _is_oauth_token(key) → OAuth on direct Anthropic → Bearer 4. else → x-api-key Also includes test fixes from PR #4051 by @erosika: - Mock sys.stdin.isatty for cmd_model TTY guard - Update camofox snapshot format mock - Fix trajectory compressor async client mock path --------- Co-authored-by: Erosika <eri@plasticlabs.ai>
296 lines
12 KiB
Python
296 lines
12 KiB
Python
"""Tests for the Camofox browser backend."""
|
|
|
|
import json
|
|
import os
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from tools.browser_camofox import (
|
|
camofox_back,
|
|
camofox_click,
|
|
camofox_close,
|
|
camofox_console,
|
|
camofox_get_images,
|
|
camofox_navigate,
|
|
camofox_press,
|
|
camofox_scroll,
|
|
camofox_snapshot,
|
|
camofox_type,
|
|
camofox_vision,
|
|
check_camofox_available,
|
|
cleanup_all_camofox_sessions,
|
|
is_camofox_mode,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxMode:
|
|
def test_disabled_by_default(self, monkeypatch):
|
|
monkeypatch.delenv("CAMOFOX_URL", raising=False)
|
|
assert is_camofox_mode() is False
|
|
|
|
def test_enabled_when_url_set(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
assert is_camofox_mode() is True
|
|
|
|
def test_health_check_unreachable(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:19999")
|
|
assert check_camofox_available() is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _mock_response(status=200, json_data=None):
|
|
resp = MagicMock()
|
|
resp.status_code = status
|
|
resp.json.return_value = json_data or {}
|
|
resp.content = b"\x89PNG\r\n\x1a\nfake"
|
|
resp.raise_for_status = MagicMock()
|
|
return resp
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Navigate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxNavigate:
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_creates_tab_on_first_navigate(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab1", "url": "https://example.com"})
|
|
|
|
result = json.loads(camofox_navigate("https://example.com", task_id="t1"))
|
|
assert result["success"] is True
|
|
assert result["url"] == "https://example.com"
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_navigates_existing_tab(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
# First call creates tab
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab2", "url": "https://a.com"})
|
|
camofox_navigate("https://a.com", task_id="t2")
|
|
|
|
# Second call navigates
|
|
mock_post.return_value = _mock_response(json_data={"ok": True, "url": "https://b.com"})
|
|
result = json.loads(camofox_navigate("https://b.com", task_id="t2"))
|
|
assert result["success"] is True
|
|
assert result["url"] == "https://b.com"
|
|
|
|
def test_connection_error_returns_helpful_message(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:19999")
|
|
result = json.loads(camofox_navigate("https://example.com", task_id="t_err"))
|
|
assert result["success"] is False
|
|
assert "Cannot connect" in result["error"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Snapshot
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxSnapshot:
|
|
def test_no_session_returns_error(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
result = json.loads(camofox_snapshot(task_id="no_such_task"))
|
|
assert result["success"] is False
|
|
assert "browser_navigate" in result["error"]
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
@patch("tools.browser_camofox.requests.get")
|
|
def test_returns_snapshot(self, mock_get, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
# Create session
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab3", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t3")
|
|
|
|
# Return snapshot
|
|
mock_get.return_value = _mock_response(json_data={
|
|
"snapshot": "- heading \"Test\" [e1]\n- button \"Submit\" [e2]",
|
|
"refsCount": 2,
|
|
})
|
|
result = json.loads(camofox_snapshot(task_id="t3"))
|
|
assert result["success"] is True
|
|
assert "[e1]" in result["snapshot"]
|
|
assert result["element_count"] == 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Click / Type / Scroll / Back / Press
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxInteractions:
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_click(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab4", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t4")
|
|
|
|
mock_post.return_value = _mock_response(json_data={"ok": True, "url": "https://x.com"})
|
|
result = json.loads(camofox_click("@e5", task_id="t4"))
|
|
assert result["success"] is True
|
|
assert result["clicked"] == "e5"
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_type(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab5", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t5")
|
|
|
|
mock_post.return_value = _mock_response(json_data={"ok": True})
|
|
result = json.loads(camofox_type("@e3", "hello world", task_id="t5"))
|
|
assert result["success"] is True
|
|
assert result["typed"] == "hello world"
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_scroll(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab6", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t6")
|
|
|
|
mock_post.return_value = _mock_response(json_data={"ok": True})
|
|
result = json.loads(camofox_scroll("down", task_id="t6"))
|
|
assert result["success"] is True
|
|
assert result["scrolled"] == "down"
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_back(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab7", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t7")
|
|
|
|
mock_post.return_value = _mock_response(json_data={"ok": True, "url": "https://prev.com"})
|
|
result = json.loads(camofox_back(task_id="t7"))
|
|
assert result["success"] is True
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_press(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab8", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t8")
|
|
|
|
mock_post.return_value = _mock_response(json_data={"ok": True})
|
|
result = json.loads(camofox_press("Enter", task_id="t8"))
|
|
assert result["success"] is True
|
|
assert result["pressed"] == "Enter"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Close
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxClose:
|
|
@patch("tools.browser_camofox.requests.delete")
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_close_session(self, mock_post, mock_delete, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab9", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t9")
|
|
|
|
mock_delete.return_value = _mock_response(json_data={"ok": True})
|
|
result = json.loads(camofox_close(task_id="t9"))
|
|
assert result["success"] is True
|
|
assert result["closed"] is True
|
|
|
|
def test_close_nonexistent_session(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
result = json.loads(camofox_close(task_id="nonexistent"))
|
|
assert result["success"] is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Console (limited support)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxConsole:
|
|
def test_console_returns_empty_with_note(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
result = json.loads(camofox_console(task_id="t_console"))
|
|
assert result["success"] is True
|
|
assert result["total_messages"] == 0
|
|
assert "not available" in result["note"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Images
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxGetImages:
|
|
@patch("tools.browser_camofox.requests.post")
|
|
@patch("tools.browser_camofox.requests.get")
|
|
def test_get_images(self, mock_get, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab10", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t10")
|
|
|
|
# camofox_get_images parses images from the accessibility tree snapshot
|
|
snapshot_text = (
|
|
'- img "Logo"\n'
|
|
' /url: https://x.com/img.png\n'
|
|
)
|
|
mock_get.return_value = _mock_response(json_data={
|
|
"snapshot": snapshot_text,
|
|
})
|
|
result = json.loads(camofox_get_images(task_id="t10"))
|
|
assert result["success"] is True
|
|
assert result["count"] == 1
|
|
assert result["images"][0]["src"] == "https://x.com/img.png"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Routing integration — verify browser_tool routes to camofox
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserToolRouting:
|
|
"""Verify that browser_tool.py delegates to camofox when CAMOFOX_URL is set."""
|
|
|
|
@patch("tools.browser_camofox.requests.post")
|
|
def test_browser_navigate_routes_to_camofox(self, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab_rt", "url": "https://example.com"})
|
|
|
|
from tools.browser_tool import browser_navigate
|
|
# Bypass SSRF check for test URL
|
|
with patch("tools.browser_tool._is_safe_url", return_value=True):
|
|
result = json.loads(browser_navigate("https://example.com", task_id="t_route"))
|
|
assert result["success"] is True
|
|
|
|
def test_check_requirements_passes_with_camofox(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
from tools.browser_tool import check_browser_requirements
|
|
assert check_browser_requirements() is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cleanup helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCamofoxCleanup:
|
|
@patch("tools.browser_camofox.requests.post")
|
|
@patch("tools.browser_camofox.requests.delete")
|
|
def test_cleanup_all(self, mock_delete, mock_post, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
mock_post.return_value = _mock_response(json_data={"tabId": "tab_c", "url": "https://x.com"})
|
|
camofox_navigate("https://x.com", task_id="t_cleanup")
|
|
|
|
mock_delete.return_value = _mock_response(json_data={"ok": True})
|
|
cleanup_all_camofox_sessions()
|
|
|
|
# Session should be gone
|
|
result = json.loads(camofox_snapshot(task_id="t_cleanup"))
|
|
assert result["success"] is False
|