"""Unit tests for agent_team.invoker (ยง3.1) โ€” all mocked, no network/SDK. These tests prove the module imports cleanly without ``claude_agent_sdk`` or ``anthropic`` installed, and exercise each billing path through the injected SDK seams (``_query``/``_options_cls``/``_client``) so nothing real is called. """ from __future__ import annotations import pytest from agent_team import billing, invoker from agent_team.billing import BillingMode, ClaudeResult @pytest.fixture(autouse=True) def _restore_invoker(): """Restore the module-global billing invoker after each test.""" original = billing._invoker yield billing._invoker = original # --------------------------------------------------------------------------- # # Fakes mirroring the Agent SDK / anthropic message shapes # --------------------------------------------------------------------------- # class _FakeTextBlock: def __init__(self, text: str) -> None: self.text = text # Class names mirror the real Agent SDK message types โ€” the invoker dispatches # on ``type(msg).__name__``, so these MUST be named AssistantMessage / # ResultMessage to be recognised. class AssistantMessage: def __init__(self, text: str) -> None: self.content = [_FakeTextBlock(text)] class ResultMessage: def __init__(self, result: str, cost: float = 0.42) -> None: self.result = result self.total_cost_usd = cost self.usage = {"input_tokens": 11, "output_tokens": 7} def _fake_options(**kwargs): """Stand-in for ClaudeAgentOptions: just record the kwargs.""" return dict(kwargs) def _make_fake_query(messages): """Build an async ``query(prompt=..., options=...)`` yielding ``messages``.""" async def _query(*, prompt, options): for msg in messages: yield msg return _query class _FakeAnthropicUsage: def __init__(self) -> None: self.input_tokens = 12 self.output_tokens = 5 def model_dump(self) -> dict: return {"input_tokens": self.input_tokens, "output_tokens": self.output_tokens} class _FakeAnthropicMessage: def __init__(self, text: str) -> None: self.content = [_FakeTextBlock(text)] self.usage = _FakeAnthropicUsage() class _FakeMessages: def __init__(self, text: str) -> None: self._text = text self.calls: list[dict] = [] def create(self, **kwargs): self.calls.append(kwargs) return _FakeAnthropicMessage(self._text) class _FakeAnthropicClient: def __init__(self, text: str) -> None: self.messages = _FakeMessages(text) # --------------------------------------------------------------------------- # # Clean import without the SDKs # --------------------------------------------------------------------------- # def test_module_imports_without_sdks() -> None: import importlib import sys # claude_agent_sdk / anthropic are not installed in this env. assert "claude_agent_sdk" not in sys.modules mod = importlib.import_module("agent_team.invoker") assert hasattr(mod, "subscription_invoker") assert hasattr(mod, "api_invoker") # --------------------------------------------------------------------------- # # Subscription path # --------------------------------------------------------------------------- # def test_subscription_invoker_returns_result( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "oauth-tok") messages = [ AssistantMessage("partial "), ResultMessage("final answer"), ] result = invoker.subscription_invoker( "hello", mode=BillingMode.SUBSCRIPTION, _query=_make_fake_query(messages), _options_cls=_fake_options, ) assert isinstance(result, ClaudeResult) assert result.text == "final answer" assert result.mode is BillingMode.SUBSCRIPTION # usage populated from ResultMessage cost + usage dict. assert result.usage["total_cost_usd"] == pytest.approx(0.42) assert result.usage["input_tokens"] == 11 # raw carries the collected message stream. assert result.raw == messages def test_subscription_invoker_falls_back_to_assistant_text( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "oauth-tok") messages = [AssistantMessage("a"), AssistantMessage("b")] result = invoker.subscription_invoker( "hi", mode=BillingMode.SUBSCRIPTION, _query=_make_fake_query(messages), _options_cls=_fake_options, ) assert result.text == "a\nb" def test_subscription_invoker_requires_oauth_token( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False) with pytest.raises(RuntimeError, match="secrev.env"): invoker.subscription_invoker( "hi", mode=BillingMode.SUBSCRIPTION, _query=_make_fake_query([]), _options_cls=_fake_options, ) # --------------------------------------------------------------------------- # # API path # --------------------------------------------------------------------------- # def test_api_invoker_returns_result() -> None: client = _FakeAnthropicClient("api text") result = invoker.api_invoker("ask", mode=BillingMode.API, _client=client) assert result.text == "api text" assert result.mode is BillingMode.API assert result.usage == {"input_tokens": 12, "output_tokens": 5} # The model + prompt were threaded into the SDK call. assert client.messages.calls[0]["model"] == invoker.API_MODEL assert client.messages.calls[0]["messages"] == [{"role": "user", "content": "ask"}] # --------------------------------------------------------------------------- # # Bedrock path # --------------------------------------------------------------------------- # def test_bedrock_raises_not_implemented() -> None: with pytest.raises(NotImplementedError, match="BEDROCK"): invoker.real_invoker("hi", mode=BillingMode.BEDROCK) # --------------------------------------------------------------------------- # # real_invoker dispatch โ€” SUBSCRIPTION + API branches forward kwargs to the leaf # --------------------------------------------------------------------------- # def test_real_invoker_dispatches_subscription_branch( monkeypatch: pytest.MonkeyPatch, ) -> None: """``real_invoker`` routes SUBSCRIPTION to ``subscription_invoker`` (kwargs fwd).""" monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "oauth-tok") messages = [AssistantMessage("partial "), ResultMessage("sub final")] result = invoker.real_invoker( "hello", mode=BillingMode.SUBSCRIPTION, _query=_make_fake_query(messages), _options_cls=_fake_options, ) assert isinstance(result, ClaudeResult) assert result.mode is BillingMode.SUBSCRIPTION assert result.text == "sub final" # the ResultMessage leaf ran assert result.raw == messages def test_real_invoker_dispatches_api_branch() -> None: """``real_invoker`` routes API to ``api_invoker`` with the injected client.""" client = _FakeAnthropicClient("api branch text") result = invoker.real_invoker("ask", mode=BillingMode.API, _client=client) assert result.mode is BillingMode.API assert result.text == "api branch text" # the anthropic-client leaf ran # The prompt was threaded through to the injected client. assert client.messages.calls[0]["messages"] == [{"role": "user", "content": "ask"}] # --------------------------------------------------------------------------- # # Binding into the billing seam # --------------------------------------------------------------------------- # def test_bind_subscription_invoker_sets_billing_invoker() -> None: invoker.bind_subscription_invoker() assert billing._invoker is invoker.real_invoker def test_bound_invoker_drives_claude_invoke( monkeypatch: pytest.MonkeyPatch, ) -> None: # Bind a fake invoker so claude_invoke routes through it end to end. captured: dict = {} def fake(prompt: str, *, mode: BillingMode, **kw): captured["prompt"] = prompt captured["mode"] = mode return ClaudeResult(text="routed", mode=mode) invoker.bind_invoker(fake) result = billing.claude_invoke("q", mode=BillingMode.API) assert result.text == "routed" assert captured["mode"] is BillingMode.API assert captured["prompt"] == "q" def test_bind_invoker_defaults_to_real_invoker() -> None: invoker.bind_invoker() assert billing._invoker is invoker.real_invoker