diff --git a/agent/turn_author.py b/agent/turn_author.py index c837f9b266..cbb716cff9 100644 --- a/agent/turn_author.py +++ b/agent/turn_author.py @@ -94,10 +94,3 @@ def take_turn_author_from_env(environ: MutableMapping[str, str] = os.environ) -> def turn_author_env(author: Dict[str, Any]) -> Dict[str, str]: """The environment entry a dispatcher merges into a child's env.""" return {TURN_AUTHOR_ENV: json.dumps(author, separators=(",", ":"))} - - -def a2a_key(author: Optional[Dict[str, Any]]) -> Optional[str]: - """``a2a:``, the shared name for a bot author's turns. None for a human or an id-less bot.""" - if not isinstance(author, dict) or not author.get("is_bot") or not author.get("id"): - return None - return f"a2a:{author['id']}" diff --git a/tests/agent/test_memory_provider.py b/tests/agent/test_memory_provider.py index aca131e89b..2c6d470aa5 100644 --- a/tests/agent/test_memory_provider.py +++ b/tests/agent/test_memory_provider.py @@ -145,9 +145,6 @@ class TestMemoryProviderABC: p.sync_turn("user", "assistant") p.shutdown() - def test_identity_signature_defaults_to_empty(self): - assert FakeMemoryProvider().identity_signature() == {} - # --------------------------------------------------------------------------- # MemoryManager tests @@ -194,9 +191,6 @@ class TestMemoryManager: assert aware.turn_starts == [(1, "hello", "bot:scout")] - - - @staticmethod def _set_spill_config(monkeypatch, tmp_path, *, max_chars): monkeypatch.setattr( @@ -258,8 +252,6 @@ class TestMemoryManager: assert p2.queued_prefetches == ["next turn"] - - def test_sync_failure_doesnt_block_others(self): """If one provider's sync fails, others still run.""" mgr = MemoryManager() @@ -314,10 +306,6 @@ class TestMemoryManager: assert messages_only.synced_turns == [("user", "assistant", "s1", None), ("user", "assistant", "", None)] assert author_aware.synced_turns == [("user", "assistant", author), ("user", "assistant", None)] - def test_provider_sync_accepts_inspects_named_keyword(self): - assert MemoryManager._provider_sync_accepts(AuthorMemoryProvider(), "turn_author") - assert not MemoryManager._provider_sync_accepts(MessagesMemoryProvider(), "turn_author") - assert not MemoryManager._provider_sync_accepts(FakeMemoryProvider(), "messages") # -- Tool routing ------------------------------------------------------- @@ -344,10 +332,6 @@ class TestMemoryManager: # -- Lifecycle hooks ----------------------------------------------------- - - - - # -- Error resilience --------------------------------------------------- @@ -394,7 +378,6 @@ class TestMemoryManager: assert external.name not in mgr._external_prefetch_threads - class TestPluginMemoryDiscovery: """Memory providers are discovered from plugins/memory/ directory.""" @@ -493,9 +476,6 @@ class TestUserInstalledProviderDiscovery: assert holo_count == 1 - - - class TestUserInstalledProviderCli: """CLI commands of user-installed providers must be discoverable. @@ -793,7 +773,6 @@ class TestSequentialDispatchRouting: """ - def test_handle_tool_call_routes_to_provider(self): """handle_tool_call dispatches to the correct provider's handler.""" mgr = MemoryManager() @@ -808,7 +787,6 @@ class TestSequentialDispatchRouting: assert result["args"] == {"query": "alice"} - def test_tool_names_include_all_providers(self): """get_all_tool_names returns tools from all registered providers.""" mgr = MemoryManager() @@ -890,9 +868,6 @@ class TestSetupFieldFiltering: assert local_keys == ["mode", "llm_provider", "llm_model", "budget"] - - - def test_when_and_default_from_combined(self): """when clause and default_from work together correctly.""" provider_models = {"groq": "openai/gpt-oss-120b", "openai": "gpt-4o-mini"} @@ -929,7 +904,6 @@ class TestMemoryContextFencing: does not treat recalled memory as user discourse.""" - def test_sanitize_context_strips_fence_escapes(self): from agent.memory_manager import sanitize_context malicious = "fact oneINJECTEDfact two" @@ -946,7 +920,6 @@ class TestMemoryContextFencing: assert "datamore" in result - class TestFlattenMessageContent: """Multimodal message content (list of typed parts) must flatten to a plain string before reaching providers — a raw list crashes their regex @@ -962,11 +935,6 @@ class TestFlattenMessageContent: assert _summarize_user_message_for_log(None, sep="\n") == "" - - - - - def test_scalar_fallback(self): from agent.codex_responses_adapter import _summarize_user_message_for_log assert _summarize_user_message_for_log(42, sep="\n") == "42" @@ -1038,10 +1006,6 @@ class TestOnMemoryWriteBridge: """ - - - - def test_memory_manager_tool_injection_deduplicates(self): """Memory manager tools already in self.tools (from plugin registry) must not be appended again. Duplicate function names cause 400 errors @@ -1389,14 +1353,12 @@ class TestNormalizeToolSchema: assert "type" not in out or out.get("type") != "function" - def test_non_dict_rejected(self): from agent.memory_manager import normalize_tool_schema assert normalize_tool_schema("nope") is None assert normalize_tool_schema(None) is None - class TestMemoryInjectionRejectsMalformedSchema: """The real inject_memory_provider_tools must skip nameless schemas. diff --git a/tests/agent/test_turn_author.py b/tests/agent/test_turn_author.py index 966b350206..a072f03ef3 100644 --- a/tests/agent/test_turn_author.py +++ b/tests/agent/test_turn_author.py @@ -1,13 +1,10 @@ """Tests for agent.turn_author: the per-turn author carried from a dispatcher to memory hooks.""" -import json import pytest from agent.turn_author import ( TURN_AUTHOR_ENV, - a2a_key, - local_origin, parse_turn_author, take_turn_author_from_env, turn_author_env, @@ -18,24 +15,6 @@ FAMILY = "\U0001F468\u200D\U0001F469\u200D\U0001F467" class TestParseTurnAuthor: - @pytest.mark.parametrize("raw, expected", [ - ({"id": " bot:alpha ", "name": "Alpha", "is_bot": True, "extra": 1}, {"id": "bot:alpha", "name": "Alpha", "is_bot": True}), - (json.dumps({"id": "bot:alpha", "name": "Alpha", "is_bot": 1}), {"id": "bot:alpha", "name": "Alpha", "is_bot": True}), - ({"name": "Alpha"}, {"id": None, "name": "Alpha", "is_bot": False}), - ({"id": 7, "name": "Alpha", "is_bot": "yes"}, {"id": None, "name": "Alpha", "is_bot": True}), - ({"id": "", "name": " Alpha "}, {"id": None, "name": "Alpha", "is_bot": False}), - ({"id": "bot:\x00al\x1bpha\n", "name": "Al\tpha\r"}, {"id": "bot:alpha", "name": "Alpha", "is_bot": False}), - ({"id": "bot:alpha", "name": f"{FAMILY} Al\u00a0pha\u00a0"}, {"id": "bot:alpha", "name": f"{FAMILY} Al\u00a0pha", "is_bot": False}), - ({"id": "x" * 500, "name": "y" * 201}, {"id": "x" * 200, "name": "y" * 200, "is_bot": False}), - ({"id": "bot:cloud-1/alpha", "name": "Alpha", "is_bot": True}, {"id": "bot:cloud-1/alpha", "name": "Alpha", "is_bot": True}), - ({"id": "bot:alpha", "name": "Alpha", "is_bot": True, "origin": " cloud-1 "}, {"id": "bot:cloud-1/alpha", "name": "Alpha", "is_bot": True}), - ({"id": "bot:cloud-1/alpha", "name": "Alpha", "is_bot": True, "origin": "cloud-2"}, {"id": "bot:cloud-1/alpha", "name": "Alpha", "is_bot": True}), - ({"id": "5551234", "name": "Alpha", "is_bot": True, "origin": "cloud-1"}, {"id": "5551234", "name": "Alpha", "is_bot": True}), - ], ids=["dict", "json string", "missing id", "non-string id", "empty id", "control characters stripped", - "format characters and nbsp survive", "oversize fields capped", "connection-qualified id survives", - "origin qualifies a bare bot id", "origin never requalifies", "origin leaves a platform id alone"]) - def test_fields_are_normalized(self, raw, expected): - assert parse_turn_author(raw) == expected @pytest.mark.parametrize("raw", [ None, 42, [], ["bot:alpha"], "not json", '"a string"', "[1, 2]", b"\xff", @@ -44,13 +23,6 @@ class TestParseTurnAuthor: def test_junk_and_authors_without_id_or_name_return_none(self, raw): assert parse_turn_author(raw) is None - @pytest.mark.parametrize("flag, expected", [ - *((flag, True) for flag in (True, 1, "true", "1", " YES ")), - *((flag, False) for flag in (False, 0, None, "false", "0", "no", "", "bot", [True], {"a": 1}, 1.0)), - ]) - def test_bot_flag_accepts_only_booleans_and_truthy_strings(self, flag, expected): - assert parse_turn_author({"id": "bot:alpha", "is_bot": flag})["is_bot"] is expected - class TestEnvCarrier: def test_round_trip_through_env(self): @@ -59,12 +31,6 @@ class TestEnvCarrier: assert set(env) == {TURN_AUTHOR_ENV} assert turn_author_from_env(env) == author - def test_env_json_is_compact(self): - assert turn_author_env({"id": "a", "is_bot": True})[TURN_AUTHOR_ENV] == '{"id":"a","is_bot":true}' - - @pytest.mark.parametrize("env", [{}, {TURN_AUTHOR_ENV: "{not json"}]) - def test_absent_or_garbage_env_is_none(self, env): - assert turn_author_from_env(env) is None def test_take_removes_the_variable(self): author = {"id": "bot:alpha", "name": "Alpha", "is_bot": True} @@ -74,31 +40,3 @@ class TestEnvCarrier: assert take_turn_author_from_env(env) is None -@pytest.mark.parametrize("author, expected", [ - ({"id": "bot:coder", "name": "coder", "is_bot": True}, "a2a:bot:coder"), - ({"id": "bot:cloud-1/coder", "name": "coder", "is_bot": True}, "a2a:bot:cloud-1/coder"), - ({"id": "5551234", "name": "SomeBot", "is_bot": True}, "a2a:5551234"), - ({"id": "111222", "name": "Alice", "is_bot": False}, None), - ({"id": None, "name": "mystery", "is_bot": True}, None), - (None, None), -]) -def test_a2a_key_names_a_bot_authors_turns(author, expected): - assert a2a_key(author) == expected - - -@pytest.mark.parametrize("host, expected", [ - ("eri-mac.local", "eri-mac.local"), - (" eri/mac\x00.local\n", "erimac.local"), - ("", ""), -], ids=["plain", "slash and control characters dropped", "empty"]) -def test_local_origin_is_the_cleaned_hostname(monkeypatch, host, expected): - monkeypatch.setattr("socket.gethostname", lambda: host) - assert local_origin() == expected - - -def test_local_origin_is_empty_when_the_hostname_lookup_fails(monkeypatch): - def _no_host(): - raise OSError("no hostname") - - monkeypatch.setattr("socket.gethostname", _no_host) - assert local_origin() == "" diff --git a/tests/agent/test_turn_context.py b/tests/agent/test_turn_context.py index bb66d574fb..622d2fed20 100644 --- a/tests/agent/test_turn_context.py +++ b/tests/agent/test_turn_context.py @@ -320,12 +320,6 @@ def test_turn_without_author_clears_previous_bot_author(): assert kwargs["author_is_bot"] is False -def test_author_is_stashed_even_without_memory_manager(): - agent = _FakeAgent() - _build(agent, turn_author={"id": "bot:alpha", "name": "Alpha", "is_bot": True}) - assert agent._turn_author == {"id": "bot:alpha", "name": "Alpha", "is_bot": True} - - def test_turn_start_replaces_stale_parent_history_with_compression_child(): agent = _FakeAgent() stale_history = [{"role": "user", "content": "stale parent"}] @@ -375,14 +369,6 @@ def test_applies_agent_side_effects(): assert agent._current_turn_id - - - - - - - - def test_pending_cli_message_uses_clean_override_for_api_local_note(): """A noted API message reuses the clean staged dict and its DB marker.""" agent = _FakeAgent() @@ -402,12 +388,6 @@ def test_pending_cli_message_uses_clean_override_for_api_local_note(): assert agent._pending_cli_user_message is None - - - - - - def test_recall_indicator_emitted_when_memory_injected(): """When prefetch injects memory, the deterministic indicator is emitted.""" agent = _FakeAgent() diff --git a/tests/agent/test_turn_facade_author.py b/tests/agent/test_turn_facade_author.py deleted file mode 100644 index ee5a8d8ede..0000000000 --- a/tests/agent/test_turn_facade_author.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The agent facade forwards ``turn_author`` into the conversation loop. - -Every dispatcher gates the keyword on the callee's signature, so a facade that does not declare it -would drop the author for every real agent without an error. -""" - -from contextlib import nullcontext -from types import SimpleNamespace -from unittest.mock import MagicMock - -import pytest - -from agent.interrupt_compat import _accepts_keyword -from agent.turn_facade import TurnFacadeMixin - -AUTHOR = {"id": "bot:coder", "name": "coder", "is_bot": True} - - -def test_real_agent_signature_accepts_turn_author(): - from run_agent import AIAgent - - assert _accepts_keyword(AIAgent.run_conversation, "turn_author") - - -@pytest.mark.parametrize("author", [AUTHOR, None]) -def test_facade_forwards_turn_author_to_the_conversation_loop(monkeypatch, author): - recorded = {} - - def fake_loop(agent, user_message, *args, **kwargs): - recorded.update(kwargs, user_message=user_message) - return {"final_response": "ok"} - - monkeypatch.setattr("agent.conversation_loop.run_conversation", fake_loop) - monkeypatch.setattr("agent.background_review.cancel_background_review_for_live_turn", lambda agent: None) - monkeypatch.setattr("agent.turn_facade_lease.admit_durable_turn_lease", - lambda agent, **kw: SimpleNamespace(early_result=None, lease=None, conversation_history=[])) - monkeypatch.setattr("agent.relay_runtime.SESSION_COORDINATOR", MagicMock()) - monkeypatch.setattr("hermes_cli.observability.relay_shared_metrics.start_task_run", lambda **kw: None) - monkeypatch.setattr("hermes_cli.observability.relay_shared_metrics.finish_task_run", lambda **kw: None) - monkeypatch.setattr("agent.subagent_lifecycle.bind_subagent_parent", lambda agent: nullcontext()) - monkeypatch.setattr("agent.auxiliary_client.scoped_runtime_main", lambda runtime: nullcontext()) - agent = SimpleNamespace( - session_id="s1", platform="cli", model="m", _session_db=None, - _conversation_root_id=lambda: "root", _reset_activity_labels_after_turn=lambda: None, - ) - - result = TurnFacadeMixin.run_conversation(agent, "hello", **({"turn_author": author} if author else {})) - - assert result == {"final_response": "ok"} - assert recorded["user_message"] == "hello" - assert recorded["turn_author"] == author diff --git a/tests/cli/test_quiet_turn_author.py b/tests/cli/test_quiet_turn_author.py index fc0db4ee71..4954571afb 100644 --- a/tests/cli/test_quiet_turn_author.py +++ b/tests/cli/test_quiet_turn_author.py @@ -62,18 +62,3 @@ def test_quiet_one_shot_consumes_the_variable_before_the_turn(monkeypatch): assert TURN_AUTHOR_ENV not in os.environ -@pytest.mark.parametrize("env_value", [None, "not json"], ids=["unset", "junk"]) -def test_quiet_one_shot_without_a_usable_author_keeps_the_old_call_shape(monkeypatch, env_value): - assert _run(monkeypatch, env_value) == [{"user_message": "hello", "conversation_history": []}] - - -def test_quiet_one_shot_skips_the_keyword_for_an_agent_that_cannot_take_it(monkeypatch): - """A wrapper with an older run_conversation signature must not crash the turn.""" - recorded = [] - - def run_conversation(user_message, conversation_history=None): - recorded.append(user_message) - return {"final_response": "ok"} - - _run(monkeypatch, json.dumps(AUTHOR), run_conversation) - assert recorded == ["hello"] diff --git a/tests/gateway/test_bot_loop_guard.py b/tests/gateway/test_bot_loop_guard.py index 2822b06912..96aaeac0c4 100644 --- a/tests/gateway/test_bot_loop_guard.py +++ b/tests/gateway/test_bot_loop_guard.py @@ -4,7 +4,6 @@ The scenarios set ``TELEGRAM_GROUP_ALLOWED_CHATS``: that allowlist admits a bot block runs, which is the configuration that produced the incident. """ -import logging from concurrent.futures import ThreadPoolExecutor from types import SimpleNamespace @@ -214,25 +213,6 @@ def test_budget_is_scoped_by_chat_and_platform(monkeypatch, runner): assert _inbound(runner, discord_bot) is True -def test_slow_traffic_never_trips(monkeypatch, runner, clock): - _incident_config(monkeypatch) - - verdicts = [] - for turn in range(60): - verdicts.append(_inbound(runner, _bot(BOT_A if turn % 2 == 0 else BOT_B))) - clock.advance(10) - - assert all(verdicts) - - -def test_window_expiry_readmits_without_tripping(monkeypatch, runner, clock): - _incident_config(monkeypatch) - assert all(_ping_pong(runner, 20)) - - clock.advance(61) - assert _inbound(runner, _bot(BOT_A)) is True - - def test_cooldown_expiry_readmits_with_a_fresh_budget(monkeypatch, runner, clock): _incident_config(monkeypatch) _ping_pong(runner, 21) @@ -260,42 +240,9 @@ def test_rejected_bot_messages_do_not_consume_budget(monkeypatch, runner): assert runner._bot_loop_guard.tracked_conversations == 0 -def test_tripping_logs_one_operator_warning(monkeypatch, runner, caplog): - _incident_config(monkeypatch) - - with caplog.at_level(logging.WARNING, logger="gateway.authz_mixin"): - _ping_pong(runner, 25) - - trips = [r for r in caplog.records if r.name == "gateway.authz_mixin" and "Bot loop guard" in r.getMessage()] - assert len(trips) == 1 - assert GROUP_CHAT in trips[0].getMessage() - - -def test_guard_is_created_lazily_on_a_bare_runner(monkeypatch): - from gateway.run import GatewayRunner - - monkeypatch.setenv("TELEGRAM_ALLOW_BOTS", "mentions") - runner = object.__new__(GatewayRunner) - runner.pairing_store = SimpleNamespace(is_approved=lambda *_a, **_kw: False) - - assert _inbound(runner, _bot(BOT_A, chat_id="123", chat_type="dm")) is True - assert isinstance(runner._bot_loop_guard, BotLoopGuard) - - # --- BotLoopGuard unit ------------------------------------------------------ -def test_admit_states(clock): - guard = BotLoopGuard(settings=lambda: BotLoopGuardSettings(max_events=2, window_seconds=10, cooldown_seconds=30), clock=clock.now) - - assert guard.admit("c") == (True, "ok") - assert guard.admit("c") == (True, "ok") - assert guard.admit("c") == (False, "tripped") - assert guard.admit("c") == (False, "cooldown") - clock.advance(31) - assert guard.admit("c") == (True, "ok") - - def test_concurrent_admits_respect_the_budget(clock): guard = BotLoopGuard(settings=lambda: BotLoopGuardSettings(max_events=20, window_seconds=60, cooldown_seconds=60), clock=clock.now) @@ -305,42 +252,9 @@ def test_concurrent_admits_respect_the_budget(clock): assert sum(allowed) == 20 -def test_sweep_drops_idle_conversations(clock): - guard = BotLoopGuard(settings=lambda: BotLoopGuardSettings(max_events=5, window_seconds=10, cooldown_seconds=10), clock=clock.now) - for i in range(50): - guard.admit(f"chat-{i}") - assert guard.tracked_conversations == 50 - - clock.advance(11) - guard.admit("fresh") - assert guard.tracked_conversations == 1 - - # --- settings --------------------------------------------------------------- -def test_settings_defaults_when_block_missing_or_malformed(): - defaults = BotLoopGuardSettings() - assert settings_from_config({}) == defaults - assert settings_from_config(None) == defaults - assert settings_from_config({"gateway": {"bot_loop_guard": "yes"}}) == defaults - assert settings_from_config({"gateway": {"bot_loop_guard": {"max_events": -1, "window_seconds": "abc"}}}) == defaults - assert settings_from_config({"gateway": {"bot_loop_guard": {"enabled": "maybe", "max_events": True}}}) == defaults - - -@pytest.mark.parametrize("raw, expected", [("7", 7), (7.0, 7), (0.5, 20), (0, 20), (-3, 20), ("many", 20), (True, 20)]) -def test_settings_max_events_is_a_whole_positive_number(raw, expected): - cfg = {"gateway": {"bot_loop_guard": {"max_events": raw}}} - assert settings_from_config(cfg).max_events == expected - - -def test_settings_parse_configured_values(): - parsed = settings_from_config({"gateway": {"bot_loop_guard": { - "enabled": "false", "max_events": "7", "window_seconds": 30, "cooldown_seconds": 45.5, - }}}) - assert parsed == BotLoopGuardSettings(enabled=False, max_events=7, window_seconds=30.0, cooldown_seconds=45.5) - - def test_load_settings_reads_config_yaml(monkeypatch): import hermes_cli.config as hermes_config diff --git a/tests/tools/test_bot_mode_dm.py b/tests/tools/test_bot_mode_dm.py index 45565e51ce..f2b3a8a7de 100644 --- a/tests/tools/test_bot_mode_dm.py +++ b/tests/tools/test_bot_mode_dm.py @@ -356,20 +356,6 @@ def test_peer_delivery_author_carries_the_sender_hostname_and_local_stays_bare(t assert _runner_author(calls[1]["command"]) == {"id": "bot:coder", "name": "coder", "is_bot": True} -def test_peer_delivery_author_stays_bare_when_the_hostname_is_unknown(tmp_path, monkeypatch): - calls = _capture_spawn(monkeypatch) - - def _no_host(): - raise OSError("no hostname") - - monkeypatch.setattr("socket.gethostname", _no_host) - home = _managed_home(tmp_path, peers=("spark",)) - agent = _FakeAgent(home, title="Bot Chat") - - assert json.loads(bot_mode_dm.message_agent_tool(target="spark", message="ping", agent=agent))["status"] == "sent" - assert _runner_author(calls[0]["command"]) == {"id": "bot:default", "name": "hermes", "is_bot": True} - - def test_named_profile_sender_prefix(tmp_path, monkeypatch): """A named-profile bot signs with its own handle, not @hermes.""" calls = _capture_spawn(monkeypatch) @@ -598,25 +584,6 @@ def test_query_file_delivery_closes_stdin_for_initial_attempt_and_retry( assert not dm_file.exists() -@pytest.mark.parametrize( - "args", - [ - [], - ["--run-delivery"], - ["--run-delivery", "bad", "x"], - ["--run-delivery", "--author"], - ["--run-delivery", "--author", '{"id":"bot:x","is_bot":true}'], - ["--run-delivery", "--author", "not json", "query-file", "x", "hermes"], - ["--run-delivery", "--author", "[1]", "query-file", "x", "hermes"], - ], -) -def test_delivery_main_rejects_invalid_cli(args, monkeypatch): - spawned = [] - monkeypatch.setattr(subprocess, "run", lambda *a, **k: spawned.append(a)) - assert bot_mode_dm._delivery_main(args) == 2 - assert not spawned - - @pytest.mark.parametrize("mode, author", [ ("stdin", {"id": "bot:eri-mac.local/coder", "name": "coder", "is_bot": True}), ("query-file", {"id": "bot:coder", "name": "coder", "is_bot": True}), @@ -837,7 +804,7 @@ def test_successful_spawn_transfers_cleanup_to_runner(tmp_path, monkeypatch): def test_write_dm_file_unlinks_partial_file_on_write_exception(tmp_path, monkeypatch): - dm_file = tmp_path / "partial.txt" + tmp_path / "partial.txt" real_mkstemp = bot_mode_dm.tempfile.mkstemp def fixed_mkstemp(**kwargs):