mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
222 lines
8.1 KiB
Python
222 lines
8.1 KiB
Python
"""Persistence tests for the Camofox browser backend.
|
|
|
|
Tests that managed persistence uses stable identity while default mode
|
|
uses random identity. Camofox automatically maps each userId to a
|
|
dedicated persistent Firefox profile on the server side.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from tools.browser_camofox import (
|
|
_drop_session,
|
|
_get_session,
|
|
_managed_persistence_enabled,
|
|
camofox_close,
|
|
camofox_navigate,
|
|
camofox_soft_cleanup,
|
|
check_camofox_available,
|
|
get_vnc_url,
|
|
)
|
|
from tools.browser_camofox_state import get_camofox_identity
|
|
|
|
|
|
def _mock_response(status=200, json_data=None):
|
|
resp = MagicMock()
|
|
resp.status_code = status
|
|
resp.json.return_value = json_data or {}
|
|
resp.raise_for_status = MagicMock()
|
|
return resp
|
|
|
|
|
|
def _enable_persistence():
|
|
"""Return a patch context that enables managed persistence via config."""
|
|
config = {"browser": {"camofox": {"managed_persistence": True}}}
|
|
return patch("tools.browser_camofox.load_config", return_value=config)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_session_state():
|
|
import tools.browser_camofox as mod
|
|
yield
|
|
with mod._sessions_lock:
|
|
mod._sessions.clear()
|
|
mod._vnc_url = None
|
|
mod._vnc_url_checked = False
|
|
|
|
|
|
class TestManagedPersistenceToggle:
|
|
def test_disabled_by_default(self):
|
|
config = {"browser": {"camofox": {"managed_persistence": False}}}
|
|
with patch("tools.browser_camofox.load_config", return_value=config):
|
|
assert _managed_persistence_enabled() is False
|
|
|
|
|
|
def test_disabled_on_config_load_error(self):
|
|
with patch("tools.browser_camofox.load_config", side_effect=Exception("fail")):
|
|
assert _managed_persistence_enabled() is False
|
|
|
|
|
|
class TestEphemeralMode:
|
|
"""Default behavior: random userId, no persistence."""
|
|
|
|
def test_session_gets_random_user_id(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
|
|
session = _get_session("task-1")
|
|
assert session["user_id"].startswith("hermes_")
|
|
assert session["managed"] is False
|
|
|
|
|
|
def test_session_reuse_within_same_task(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
|
|
s1 = _get_session("task-1")
|
|
s2 = _get_session("task-1")
|
|
assert s1 is s2
|
|
|
|
|
|
class TestManagedPersistenceMode:
|
|
"""With managed_persistence: stable userId derived from Hermes profile."""
|
|
|
|
def test_session_gets_stable_user_id(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
|
|
with _enable_persistence():
|
|
session = _get_session("task-1")
|
|
expected = get_camofox_identity("task-1")
|
|
assert session["user_id"] == expected["user_id"]
|
|
assert session["session_key"] == expected["session_key"]
|
|
assert session["managed"] is True
|
|
|
|
|
|
def test_navigate_reuses_identity_after_close(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
|
|
requests_seen = []
|
|
|
|
def _capture_post(url, json=None, timeout=None, headers=None):
|
|
requests_seen.append(json)
|
|
return _mock_response(
|
|
json_data={"tabId": f"tab-{len(requests_seen)}", "url": "https://example.com"}
|
|
)
|
|
|
|
with (
|
|
_enable_persistence(),
|
|
patch("tools.browser_camofox.requests.post", side_effect=_capture_post),
|
|
patch("tools.browser_camofox.requests.delete", return_value=_mock_response()),
|
|
):
|
|
first = json.loads(camofox_navigate("https://example.com", task_id="task-1"))
|
|
camofox_close("task-1")
|
|
second = json.loads(camofox_navigate("https://example.com", task_id="task-1"))
|
|
|
|
assert first["success"] is True
|
|
assert second["success"] is True
|
|
tab_requests = [req for req in requests_seen if "userId" in req]
|
|
assert len(tab_requests) == 2
|
|
assert tab_requests[0]["userId"] == tab_requests[1]["userId"]
|
|
|
|
|
|
class TestConfiguredCamofoxIdentity:
|
|
"""Externally managed Camofox sessions can provide their own identity."""
|
|
|
|
def test_env_identity_overrides_default_identity(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
monkeypatch.setenv("CAMOFOX_USER_ID", "shared-camofox")
|
|
monkeypatch.setenv("CAMOFOX_SESSION_KEY", "visible-tab")
|
|
monkeypatch.setenv("CAMOFOX_ADOPT_EXISTING_TAB", "true")
|
|
|
|
with patch("tools.browser_camofox._get", return_value={"tabs": []}) as mock_get:
|
|
session = _get_session("task-1")
|
|
|
|
assert session["user_id"] == "shared-camofox"
|
|
assert session["session_key"] == "visible-tab"
|
|
assert session["managed"] is True
|
|
assert session["adopt_existing_tab"] is True
|
|
mock_get.assert_called_once_with(
|
|
"/tabs",
|
|
params={"userId": "shared-camofox"},
|
|
timeout=5,
|
|
)
|
|
|
|
|
|
def test_soft_cleanup_preserves_externally_managed_session(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
monkeypatch.setenv("CAMOFOX_USER_ID", "shared-camofox")
|
|
|
|
with patch("tools.browser_camofox._get", return_value={"tabs": []}):
|
|
_get_session("task-1")
|
|
result = camofox_soft_cleanup("task-1")
|
|
|
|
assert result is True
|
|
import tools.browser_camofox as mod
|
|
with mod._sessions_lock:
|
|
assert "task-1" not in mod._sessions
|
|
|
|
|
|
class TestVncUrlDiscovery:
|
|
"""VNC URL is derived from the Camofox health endpoint."""
|
|
|
|
def test_vnc_url_from_health_port(self, monkeypatch):
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://myhost:9377")
|
|
health_resp = _mock_response(json_data={"ok": True, "vncPort": 6080})
|
|
with patch("tools.browser_camofox.requests.get", return_value=health_resp):
|
|
assert check_camofox_available() is True
|
|
assert get_vnc_url() == "http://myhost:6080"
|
|
|
|
|
|
def test_navigate_includes_vnc_hint(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
import tools.browser_camofox as mod
|
|
mod._vnc_url = "http://localhost:6080"
|
|
mod._vnc_url_checked = True
|
|
|
|
with patch("tools.browser_camofox.requests.post", return_value=_mock_response(
|
|
json_data={"tabId": "t1", "url": "https://example.com"}
|
|
)):
|
|
result = json.loads(camofox_navigate("https://example.com", task_id="vnc-test"))
|
|
|
|
assert result["vnc_url"] == "http://localhost:6080"
|
|
assert "vnc_hint" in result
|
|
|
|
|
|
class TestCamofoxSoftCleanup:
|
|
"""camofox_soft_cleanup drops local state only when managed persistence is on."""
|
|
|
|
def test_returns_true_and_drops_session_when_enabled(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
|
|
with _enable_persistence():
|
|
_get_session("task-1")
|
|
result = camofox_soft_cleanup("task-1")
|
|
|
|
assert result is True
|
|
# Session should have been dropped from in-memory store
|
|
import tools.browser_camofox as mod
|
|
with mod._sessions_lock:
|
|
assert "task-1" not in mod._sessions
|
|
|
|
|
|
def test_does_not_call_server_delete(self, tmp_path, monkeypatch):
|
|
"""Soft cleanup must never hit the Camofox /sessions DELETE endpoint."""
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
|
|
|
|
with (
|
|
_enable_persistence(),
|
|
patch("tools.browser_camofox.requests.delete") as mock_delete,
|
|
):
|
|
_get_session("task-1")
|
|
camofox_soft_cleanup("task-1")
|
|
|
|
mock_delete.assert_not_called()
|