From 775062e6dccb047b6ddbe858136375dbf716bcc1 Mon Sep 17 00:00:00 2001 From: Erosika Date: Mon, 30 Mar 2026 18:06:26 -0400 Subject: [PATCH 1/3] fix(tests): mock sys.stdin.isatty for cmd_model TTY guard --- tests/test_cli_provider_resolution.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_cli_provider_resolution.py b/tests/test_cli_provider_resolution.py index 667cd33a6c061..b9960f08c2c7e 100644 --- a/tests/test_cli_provider_resolution.py +++ b/tests/test_cli_provider_resolution.py @@ -424,6 +424,7 @@ def _resolve_provider(requested, **kwargs): monkeypatch.setattr("hermes_cli.auth.resolve_provider", _resolve_provider) monkeypatch.setattr(hermes_main, "_prompt_provider_choice", lambda choices: len(choices) - 1) + monkeypatch.setattr("sys.stdin", type("FakeTTY", (), {"isatty": lambda self: True})()) hermes_main.cmd_model(SimpleNamespace()) output = capsys.readouterr().out From 964956780b06743cff645b5a75a68b80672f66e7 Mon Sep 17 00:00:00 2001 From: Erosika Date: Mon, 30 Mar 2026 18:11:14 -0400 Subject: [PATCH 2/3] 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 --- tests/test_trajectory_compressor.py | 5 +++-- tests/tools/test_browser_camofox.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_trajectory_compressor.py b/tests/test_trajectory_compressor.py index c95a3af94c53b..72708b8d9c93e 100644 --- a/tests/test_trajectory_compressor.py +++ b/tests/test_trajectory_compressor.py @@ -405,12 +405,13 @@ def test_generate_summary_handles_none_content(self): @pytest.mark.asyncio async def test_generate_summary_async_handles_none_content(self): tc = _make_compressor() - tc.async_client = MagicMock() - tc.async_client.chat.completions.create = AsyncMock( + mock_client = MagicMock() + mock_client.chat.completions.create = AsyncMock( return_value=SimpleNamespace( choices=[SimpleNamespace(message=SimpleNamespace(content=None))] ) ) + tc._get_async_client = MagicMock(return_value=mock_client) metrics = TrajectoryMetrics() summary = await tc._generate_summary_async("Turn content", metrics) diff --git a/tests/tools/test_browser_camofox.py b/tests/tools/test_browser_camofox.py index a59862b9bd2af..f9ff0e7c75ff7 100644 --- a/tests/tools/test_browser_camofox.py +++ b/tests/tools/test_browser_camofox.py @@ -235,8 +235,13 @@ def test_get_images(self, mock_get, mock_post, monkeypatch): 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={ - "images": [{"src": "https://x.com/img.png", "alt": "Logo"}], + "snapshot": snapshot_text, }) result = json.loads(camofox_get_images(task_id="t10")) assert result["success"] is True From 60e9ad966448ce6b25cc06291a1435a99f9a4f57 Mon Sep 17 00:00:00 2001 From: Erosika Date: Mon, 30 Mar 2026 18:17:21 -0400 Subject: [PATCH 3/3] fix(honcho): skip Honcho writes for cron sessions (#4052) Cron prompts contain system instructions ('You are Hermes...') that get written to Honcho as user messages via _honcho_sync(), causing the dialectic model to misattribute agent traits to the user peer. Guard both write paths: - _honcho_sync(): skip when platform == 'cron' - _honcho_save_user_observation(): reject when platform == 'cron' This preserves Honcho read access (context injection) for cron sessions while blocking the writes that corrupt user representations. Closes #4052 --- run_agent.py | 11 +++ tests/test_honcho_cron_write_guard.py | 121 ++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 tests/test_honcho_cron_write_guard.py diff --git a/run_agent.py b/run_agent.py index 794c9f67ab4db..f9039e96ebcfd 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2497,6 +2497,11 @@ def _honcho_save_user_observation(self, content: str) -> str: """ if not content or not content.strip(): return json.dumps({"success": False, "error": "Content cannot be empty."}) + # Block cron sessions from writing observations — same rationale as + # _honcho_sync: cron-generated observations are agent-internal, not + # user statements. See #4052. + if self.platform == "cron": + return json.dumps({"success": False, "error": "Honcho writes disabled for cron sessions."}) try: session = self._honcho.get_or_create(self._honcho_session_key) session.add_message("user", f"[observation] {content.strip()}") @@ -2514,6 +2519,12 @@ def _honcho_sync(self, user_content: str, assistant_content: str) -> None: """Sync the user/assistant message pair to Honcho.""" if not self._honcho or not self._honcho_session_key: return + # Skip Honcho writes for cron sessions — cron prompts contain system + # instructions ("You are Hermes...") that would be misattributed to + # the user peer, corrupting the user representation. See #4052. + if self.platform == "cron": + logger.debug("Skipping Honcho sync for cron session") + return try: session = self._honcho.get_or_create(self._honcho_session_key) session.add_message("user", user_content) diff --git a/tests/test_honcho_cron_write_guard.py b/tests/test_honcho_cron_write_guard.py new file mode 100644 index 0000000000000..0ef88f5f0cb15 --- /dev/null +++ b/tests/test_honcho_cron_write_guard.py @@ -0,0 +1,121 @@ +"""Tests for Honcho cron session write guard (#4052). + +Cron sessions must not write messages to Honcho — the cron prompt +contains system instructions ("You are Hermes...") that would be +misattributed to the user peer, corrupting the user representation. + +Verifies that: +1. _honcho_sync() is a no-op when platform == "cron" +2. _honcho_save_user_observation() rejects writes when platform == "cron" +3. Non-cron sessions still sync normally +""" + +import json +import types +import sys +import pytest +from unittest.mock import MagicMock, patch + + +@pytest.fixture(autouse=True) +def _stub_deps(monkeypatch): + """Stub heavy dependencies so run_agent can import without side effects.""" + for mod_name in ( + "dotenv", + "yaml", + "rich", + "rich.console", + "rich.panel", + "rich.markdown", + "rich.syntax", + "rich.live", + "rich.text", + "rich.table", + "rich.box", + "rich.theme", + ): + if mod_name not in sys.modules: + monkeypatch.setitem(sys.modules, mod_name, types.ModuleType(mod_name)) + + fake_dotenv = sys.modules["dotenv"] + fake_dotenv.load_dotenv = lambda *a, **kw: None + + +def _make_agent(platform: str = "cron"): + """Create a minimal AIAgent-like object with Honcho state wired up.""" + from run_agent import AIAgent + + agent = object.__new__(AIAgent) + agent.platform = platform + agent.quiet_mode = True + + # Mock Honcho session manager + agent._honcho = MagicMock() + agent._honcho_session_key = "test-session" + + mock_session = MagicMock() + mock_session.messages = [] + agent._honcho.get_or_create.return_value = mock_session + + return agent, mock_session + + +class TestHonchoSyncCronGuard: + """_honcho_sync must skip writes for cron sessions.""" + + def test_cron_session_skips_sync(self): + agent, mock_session = _make_agent(platform="cron") + agent._honcho_sync("You are Hermes, an AI assistant...", "Sure, here's the result.") + + # Should never touch the session + agent._honcho.get_or_create.assert_not_called() + mock_session.add_message.assert_not_called() + agent._honcho.save.assert_not_called() + + def test_non_cron_session_syncs_normally(self): + agent, mock_session = _make_agent(platform="telegram") + agent._honcho_sync("Hello!", "Hi there!") + + agent._honcho.get_or_create.assert_called_once_with("test-session") + assert mock_session.add_message.call_count == 2 + mock_session.add_message.assert_any_call("user", "Hello!") + mock_session.add_message.assert_any_call("assistant", "Hi there!") + agent._honcho.save.assert_called_once() + + def test_cli_session_syncs_normally(self): + agent, mock_session = _make_agent(platform="cli") + agent._honcho_sync("What's the weather?", "I don't have weather tools.") + + agent._honcho.get_or_create.assert_called_once() + assert mock_session.add_message.call_count == 2 + + def test_none_platform_syncs_normally(self): + """Platform=None (e.g. direct AIAgent usage) should still sync.""" + agent, mock_session = _make_agent(platform=None) + agent._honcho_sync("test", "response") + + agent._honcho.get_or_create.assert_called_once() + + +class TestHonchoObservationCronGuard: + """_honcho_save_user_observation must reject writes for cron sessions.""" + + def test_cron_session_rejects_observation(self): + agent, mock_session = _make_agent(platform="cron") + result = json.loads(agent._honcho_save_user_observation("User prefers dark mode")) + + assert result["success"] is False + assert "cron" in result["error"].lower() + agent._honcho.get_or_create.assert_not_called() + + def test_non_cron_session_saves_observation(self): + agent, mock_session = _make_agent(platform="telegram") + result = json.loads(agent._honcho_save_user_observation("User prefers dark mode")) + + assert result["success"] is True + agent._honcho.get_or_create.assert_called_once() + mock_session.add_message.assert_called_once() + # Verify the observation prefix is present + call_args = mock_session.add_message.call_args + assert call_args[0][0] == "user" + assert "[observation]" in call_args[0][1]