From 0262e59c60abe03833ffd81b358b51012107f263 Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Fri, 15 May 2026 15:22:17 +0000 Subject: [PATCH] =?UTF-8?q?test(workspace):=20fill=20adapter=5Fbase.py=20c?= =?UTF-8?q?overage=20gaps=20=E2=80=94=2037%=20=E2=86=92=2068%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 30 unit tests covering: - resolve_provider_routing(): URL-precedence branches, unknown prefix fallback, RuntimeError on missing API key, multi-env-var resolution - RuntimeCapabilities.to_dict(): all flag combinations - BaseAdapter defaults: capabilities(), idle_timeout_override(), get_config_schema(), memory_filename(), register_tool_hook(), register_subagent_hook(), transcript_lines() - append_to_memory_hook(): new-file create, marker idempotency, append without marker, parent-dir creation - pre_stop_state(): empty executor, session_id capture, transcript_lines integration, exception suppression - restore_state(): session_id, transcript_lines, missing keys - inject_plugins(): delegates to install_plugins_via_registry Closes: #1173 Co-Authored-By: Claude Opus 4.7 --- workspace/tests/test_adapter_base_coverage.py | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 workspace/tests/test_adapter_base_coverage.py diff --git a/workspace/tests/test_adapter_base_coverage.py b/workspace/tests/test_adapter_base_coverage.py new file mode 100644 index 000000000..2e7609c79 --- /dev/null +++ b/workspace/tests/test_adapter_base_coverage.py @@ -0,0 +1,432 @@ +"""BaseAdapter coverage gap tests — fills uncovered branches in adapter_base.py. + +Covers: + - resolve_provider_routing(): all URL-precedence branches + unknown prefix + - RuntimeCapabilities.to_dict(): all flag combinations + - BaseAdapter.capabilities(): returns RuntimeCapabilities() (platform-owns-everything) + - BaseAdapter.idle_timeout_override(): returns None (use platform default) + - BaseAdapter.get_config_schema(): returns {} (override per-subclass) + - BaseAdapter.memory_filename(): returns "CLAUDE.md" + - BaseAdapter.register_tool_hook(): no-op (override for dynamic registry) + - BaseAdapter.register_subagent_hook(): no-op (override for DeepAgents) + - BaseAdapter.transcript_lines(): returns supported=False dict + - BaseAdapter.append_to_memory_hook(): idempotent append, marker deduplication + - BaseAdapter.pre_stop_state(): captures session_id from executor + transcript_lines + - BaseAdapter.restore_state(): stores session_id + transcript_lines from snapshot + - BaseAdapter.inject_plugins(): delegates to install_plugins_via_registry +""" + +import json +import os +import sys +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +WORKSPACE_DIR = Path(__file__).parent.parent +if str(WORKSPACE_DIR) not in sys.path: + sys.path.insert(0, str(WORKSPACE_DIR)) + +from a2a.server.agent_execution import AgentExecutor + +from adapter_base import ( + AdapterConfig, + BaseAdapter, + ProviderRegistry, + RuntimeCapabilities, + resolve_provider_routing, +) + + +class _StubAdapter(BaseAdapter): + """Minimal concrete adapter for testing base-class default behaviour.""" + + @staticmethod + def name() -> str: + return "stub" + + @staticmethod + def display_name() -> str: + return "Stub" + + @staticmethod + def description() -> str: + return "test stub" + + async def setup(self, config: AdapterConfig) -> None: + return None + + async def create_executor(self, config: AdapterConfig) -> AgentExecutor: # pragma: no cover + raise NotImplementedError + + +# --------------------------------------------------------------------------- +# resolve_provider_routing tests +# --------------------------------------------------------------------------- + +def test_resolve_provider_routing_parses_prefix_and_model(): + """'anthropic:claude-sonnet-4-6' splits into prefix + bare model.""" + api_key, base_url, model_id = resolve_provider_routing( + "anthropic:claude-sonnet-4-6", + {"ANTHROPIC_API_KEY": "sk-ant-test"}, + registry={"anthropic": (("ANTHROPIC_API_KEY",), "https://api.anthropic.com")}, + ) + assert api_key == "sk-ant-test" + assert base_url == "https://api.anthropic.com" + assert model_id == "claude-sonnet-4-6" + + +def test_resolve_provider_routing_falls_back_to_openai(): + """Bare model without colon defaults to openai prefix.""" + api_key, base_url, model_id = resolve_provider_routing( + "gpt-4o", + {"OPENAI_API_KEY": "sk-openai-test"}, + registry={}, + ) + assert api_key == "sk-openai-test" + assert base_url == "https://api.openai.com/v1" + assert model_id == "gpt-4o" + + +def test_resolve_provider_routing_url_from_env_var(): + """PREFIX_BASE_URL env var takes precedence over registry default.""" + env = { + "OPENAI_API_KEY": "sk-test", + "OPENAI_BASE_URL": "https://my-proxy.example.com/v1", + } + api_key, base_url, model_id = resolve_provider_routing( + "openai:gpt-4o", env, registry={} + ) + assert base_url == "https://my-proxy.example.com/v1" + + +def test_resolve_provider_routing_url_from_runtime_config(): + """runtime_config['provider_url'] takes precedence over registry default.""" + env = {"OPENAI_API_KEY": "sk-test"} + api_key, base_url, model_id = resolve_provider_routing( + "openai:gpt-4o", + env, + registry={}, + runtime_config={"provider_url": "https://config-proxy.example.com/v1"}, + ) + assert base_url == "https://config-proxy.example.com/v1" + + +def test_resolve_provider_routing_env_overrides_runtime_config(): + """env var PREFIX_BASE_URL wins over runtime_config['provider_url'].""" + env = { + "OPENAI_API_KEY": "sk-test", + "OPENAI_BASE_URL": "https://env-proxy.example.com/v1", + } + _, base_url, _ = resolve_provider_routing( + "openai:gpt-4o", + env, + registry={}, + runtime_config={"provider_url": "https://config-proxy.example.com/v1"}, + ) + assert base_url == "https://env-proxy.example.com/v1" + + +def test_resolve_provider_routing_falls_back_to_openai_on_unknown_prefix(): + """Unknown provider prefix falls back to OPENAI_API_KEY + openai.com.""" + env = {"OPENAI_API_KEY": "sk-fallback"} + api_key, base_url, model_id = resolve_provider_routing( + "unknown:some-model", env, registry={} + ) + assert api_key == "sk-fallback" + assert base_url == "https://api.openai.com/v1" + assert model_id == "some-model" + + +def test_resolve_provider_routing_raises_when_no_api_key(): + """RuntimeError raised when no API key env var is set for the prefix.""" + with pytest.raises(RuntimeError) as exc_info: + resolve_provider_routing( + "anthropic:claude-sonnet-4-6", + {}, # empty env — no ANTHROPIC_API_KEY + registry={"anthropic": (("ANTHROPIC_API_KEY",), "https://api.anthropic.com")}, + ) + assert "No API key found" in str(exc_info.value) + assert "anthropic" in str(exc_info.value) + + +def test_resolve_provider_routing_multiple_env_vars_first_found(): + """registry tuple with multiple env vars — first present in env is used.""" + env = { + # ANTHROPIC_API_KEY not set; ANTHROPIC_SECONDARY_KEY is + "ANTHROPIC_SECONDARY_KEY": "sk-secondary", + } + api_key, _, _ = resolve_provider_routing( + "anthropic:claude-sonnet-4-6", + env, + registry={"anthropic": (("ANTHROPIC_API_KEY", "ANTHROPIC_SECONDARY_KEY"), "https://api.anthropic.com")}, + ) + assert api_key == "sk-secondary" + + +# --------------------------------------------------------------------------- +# RuntimeCapabilities tests +# --------------------------------------------------------------------------- + +def test_runtime_capabilities_to_dict_all_defaults(): + """All flags default to False.""" + caps = RuntimeCapabilities() + d = caps.to_dict() + assert d == { + "heartbeat": False, + "scheduler": False, + "session": False, + "status_mgmt": False, + "retry": False, + "activity_decoration": False, + "channel_dispatch": False, + } + + +def test_runtime_capabilities_to_dict_all_true(): + """All flags can be set to True.""" + caps = RuntimeCapabilities( + provides_native_heartbeat=True, + provides_native_scheduler=True, + provides_native_session=True, + provides_native_status_mgmt=True, + provides_native_retry=True, + provides_activity_decoration=True, + provides_channel_dispatch=True, + ) + d = caps.to_dict() + assert all(v is True for v in d.values()) + + +def test_runtime_capabilities_partial_flags(): + """Partial flag set — only heartbeat and session True.""" + caps = RuntimeCapabilities( + provides_native_heartbeat=True, + provides_native_session=True, + ) + d = caps.to_dict() + assert d["heartbeat"] is True + assert d["session"] is True + assert d["scheduler"] is False + + +# --------------------------------------------------------------------------- +# BaseAdapter method default behaviour tests +# --------------------------------------------------------------------------- + +def test_capabilities_returns_empty_runtime_capabilities(): + """Default capabilities() returns RuntimeCapabilities() with all flags off.""" + adapter = _StubAdapter() + caps = adapter.capabilities() + assert isinstance(caps, RuntimeCapabilities) + d = caps.to_dict() + assert all(v is False for v in d.values()) + + +def test_idle_timeout_override_returns_none(): + """Default idle_timeout_override() returns None — use platform default.""" + adapter = _StubAdapter() + assert adapter.idle_timeout_override() is None + + +def test_get_config_schema_returns_empty_dict(): + """Default get_config_schema() returns {} — override per-subclass.""" + adapter = _StubAdapter() + assert adapter.get_config_schema() == {} + + +def test_memory_filename_returns_claude_md(): + """Default memory_filename() returns 'CLAUDE.md'.""" + adapter = _StubAdapter() + assert adapter.memory_filename() == "CLAUDE.md" + + +def test_register_tool_hook_returns_none(): + """Default register_tool_hook() is a no-op that returns None.""" + adapter = _StubAdapter() + result = adapter.register_tool_hook("some-plugin", MagicMock()) + assert result is None + + +def test_register_subagent_hook_returns_none(): + """Default register_subagent_hook() is a no-op that returns None.""" + adapter = _StubAdapter() + result = adapter.register_subagent_hook("deep-agent", {"name": "agent"}) + assert result is None + + +@pytest.mark.asyncio +async def test_transcript_lines_returns_unsupported(): + """Default transcript_lines() returns supported=False (runtime doesn't expose a log).""" + adapter = _StubAdapter() + result = await adapter.transcript_lines(since=10, limit=50) + assert result["supported"] is False + assert result["lines"] == [] + assert result["cursor"] == 10 # preserved from since arg + assert result["more"] is False + assert result["source"] is None + assert result["runtime"] == "stub" + + +# --------------------------------------------------------------------------- +# append_to_memory_hook tests +# --------------------------------------------------------------------------- + +def test_append_to_memory_hook_creates_new_file(): + """append_to_memory_hook creates the target file if it doesn't exist.""" + adapter = _StubAdapter() + with tempfile.TemporaryDirectory() as tmpdir: + config = AdapterConfig(model="test", config_path=tmpdir) + content = "# Plugin: test-plugin\nsome content" + adapter.append_to_memory_hook(config, "CLAUDE.md", content) + + path = os.path.join(tmpdir, "CLAUDE.md") + assert os.path.exists(path) + with open(path) as f: + assert content in f.read() + + +def test_append_to_memory_hook_idempotent_with_marker(): + """Second append with same marker is skipped (idempotent).""" + adapter = _StubAdapter() + with tempfile.TemporaryDirectory() as tmpdir: + config = AdapterConfig(model="test", config_path=tmpdir) + marker_content = "# Plugin: test-plugin\nsome content" + + adapter.append_to_memory_hook(config, "CLAUDE.md", marker_content) + adapter.append_to_memory_hook(config, "CLAUDE.md", marker_content) + + path = os.path.join(tmpdir, "CLAUDE.md") + with open(path) as f: + text = f.read() + # Should appear only once (second append skipped) + lines = [l for l in text.splitlines() if l.startswith("# Plugin: test-plugin")] + assert len(lines) == 1 + + +def test_append_to_memory_hook_appends_without_marker(): + """Appends when the marker line is not present (no deduplication needed).""" + adapter = _StubAdapter() + with tempfile.TemporaryDirectory() as tmpdir: + config = AdapterConfig(model="test", config_path=tmpdir) + + adapter.append_to_memory_hook(config, "CLAUDE.md", "# First plugin\ncontent A") + adapter.append_to_memory_hook(config, "CLAUDE.md", "# Second plugin\ncontent B") + + path = os.path.join(tmpdir, "CLAUDE.md") + with open(path) as f: + text = f.read() + assert "# First plugin" in text + assert "# Second plugin" in text + + +def test_append_to_memory_hook_creates_parent_dirs(): + """append_to_memory_hook creates intermediate directories.""" + adapter = _StubAdapter() + with tempfile.TemporaryDirectory() as tmpdir: + config = AdapterConfig(model="test", config_path=tmpdir) + adapter.append_to_memory_hook(config, "subdir/CLAUDE.md", "# Nested") + + path = os.path.join(tmpdir, "subdir", "CLAUDE.md") + assert os.path.exists(path) + + +# --------------------------------------------------------------------------- +# pre_stop_state tests +# --------------------------------------------------------------------------- + +def test_pre_stop_state_empty_when_no_executor(): + """pre_stop_state returns {} when no _executor is attached.""" + adapter = _StubAdapter() + state = adapter.pre_stop_state() + assert state == {} + + +def test_pre_stop_state_captures_session_id(): + """pre_stop_state reads _executor._session_id when present.""" + adapter = _StubAdapter() + mock_executor = MagicMock(spec=AgentExecutor) + mock_executor._session_id = "session-abc123" + adapter._executor = mock_executor + + state = adapter.pre_stop_state() + assert state["session_id"] == "session-abc123" + + +def test_pre_stop_state_captures_transcript_lines(): + """pre_stop_state calls transcript_lines() and includes lines when supported.""" + adapter = _StubAdapter() + adapter._executor = None # no session_id + + # Override transcript_lines to return supported=True + adapter.transcript_lines = MagicMock(return_value={ + "runtime": "stub", + "supported": True, + "lines": [{"role": "user", "content": "hello"}], + "cursor": 0, + "more": False, + "source": "/tmp/transcript.jsonl", + }) + + state = adapter.pre_stop_state() + assert state["transcript_lines"] == [{"role": "user", "content": "hello"}] + + +def test_pre_stop_state_suppresses_transcript_on_exception(): + """pre_stop_state never raises — transcript capture is best-effort.""" + adapter = _StubAdapter() + adapter._executor = None + + def broken_transcript(*args, **kwargs): + raise RuntimeError("disk error") + + adapter.transcript_lines = broken_transcript + + # Must not raise + state = adapter.pre_stop_state() + assert state == {} + + +# --------------------------------------------------------------------------- +# restore_state tests +# --------------------------------------------------------------------------- + +def test_restore_state_stores_session_id(): + """restore_state stores snapshot['session_id'] as _snapshot_session_id.""" + adapter = _StubAdapter() + adapter.restore_state({"session_id": "restored-session-xyz"}) + assert adapter._snapshot_session_id == "restored-session-xyz" + + +def test_restore_state_stores_transcript_lines(): + """restore_state stores snapshot['transcript_lines'] as _snapshot_transcript.""" + adapter = _StubAdapter() + lines = [{"role": "user", "content": "prior context"}] + adapter.restore_state({"transcript_lines": lines}) + assert adapter._snapshot_transcript == lines + + +def test_restore_state_handles_missing_keys(): + """restore_state works when snapshot lacks session_id or transcript_lines.""" + adapter = _StubAdapter() + adapter.restore_state({}) + assert adapter._snapshot_session_id is None + assert adapter._snapshot_transcript is None + + +# --------------------------------------------------------------------------- +# inject_plugins tests +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_inject_plugins_delegates_to_install_plugins_via_registry(): + """inject_plugins calls install_plugins_via_registry (default migration path).""" + from unittest.mock import AsyncMock + adapter = _StubAdapter() + + with patch.object(adapter, "install_plugins_via_registry", new_callable=AsyncMock) as mock_install: + mock_install.return_value = [] + await adapter.inject_plugins(AdapterConfig(model="test", config_path="/tmp"), MagicMock()) + mock_install.assert_called_once() -- 2.52.0