Files
hermes-webui/tests/test_agent_v021_transport_normalize.py
T
Sebastian B 6a0defbc2f test: agent-less CI compat for transport-port tests
The new normalize test now stubs agent.anthropic_adapter in sys.modules
(CI has no agent package — real import raised ModuleNotFoundError), and the
issue-1013 codex fake ports to the v0.21 _get_transport contract instead of
the deleted _normalize_codex_response method.
2026-09-02 15:29:04 +01:00

211 lines
7.5 KiB
Python

"""agent v0.21 transport-API port tests for out-of-band LLM completions.
hermes-agent v0.21 removed ``agent.anthropic_adapter.normalize_anthropic_response``
and ``AIAgent._normalize_codex_response``. The sanctioned out-of-band contract is
``agent._get_transport(<mode>).normalize_response(...)`` returning a
``NormalizedResponse`` (``.content``).
These tests pin the two out-of-band consumers against a v0.21-shaped fake agent
(no legacy normalizer symbols exist; the transport normalizes):
- ``api.streaming.generate_title_raw_via_agent`` (anthropic_messages branch)
- the nested ``_agent_text_completion`` inside
``api.routes._handle_handoff_summary`` (codex_responses branch)
On the pre-port code both branches die (ImportError / AttributeError) and
silently fall back to empty titles / local fallback summaries; these tests
fail in that state.
"""
import sys
import types
TITLE_TEXT = 'Claude Transport Title'
SUMMARY_TEXT = '- The remaining work is the final review pass.'
class _FakeTransport:
"""v0.21-shaped transport: normalize_response returns a NormalizedResponse-like object."""
def __init__(self, content):
self._content = content
self.calls = []
def normalize_response(self, resp, **kwargs):
self.calls.append((resp, kwargs))
return types.SimpleNamespace(content=self._content)
def _make_anthropic_fake_agent(transport):
"""Fake agent exposing only the v0.21 surface the title branch needs."""
class _FakeAgent:
api_mode = 'anthropic_messages'
model = 'anthropic/claude-sonnet-4-5'
provider = 'anthropic'
base_url = ''
def __init__(self):
self.reasoning_config = None
self._is_anthropic_oauth = True
def _anthropic_preserve_dots(self):
return False
def _anthropic_messages_create(self, api_kwargs):
# Opaque response blob; normalizing is the transport's job on v0.21.
return object()
def _get_transport(self, mode=None):
assert mode is None
return transport
return _FakeAgent()
def test_anthropic_title_uses_transport_normalize_response(monkeypatch):
"""The anthropic_messages title branch must normalize via the agent transport.
v0.21 deleted ``normalize_anthropic_response``; the branch must not import it.
"""
from api.streaming import generate_title_raw_via_agent
# Simulate the v0.21 adapter module in-process (works in agent-less CI too):
# it still exports build_anthropic_kwargs but deliberately does NOT define
# normalize_anthropic_response — exactly the environment this port targets.
fake_adapter = types.ModuleType('agent.anthropic_adapter')
fake_adapter.build_anthropic_kwargs = lambda **kwargs: {}
fake_agent_pkg = sys.modules.get('agent') or types.ModuleType('agent')
fake_agent_pkg.anthropic_adapter = fake_adapter
monkeypatch.setitem(sys.modules, 'agent', fake_agent_pkg)
monkeypatch.setitem(sys.modules, 'agent.anthropic_adapter', fake_adapter)
transport = _FakeTransport(TITLE_TEXT)
agent = _make_anthropic_fake_agent(transport)
result, status = generate_title_raw_via_agent(
agent,
user_text='Hey nur ein kurzer Test',
assistant_text='Alles klar, ich helfe dir dabei.',
)
assert result == TITLE_TEXT
assert status == 'llm'
assert transport.calls, 'transport normalize_response must be called'
assert transport.calls[0][1].get('strip_tool_prefix') is True
def test_codex_handoff_summary_uses_transport_normalize_response(monkeypatch):
"""The codex_responses handoff branch must normalize via the agent transport.
v0.21 deleted ``AIAgent._normalize_codex_response``; the branch must use
``agent._get_transport('codex_responses').normalize_response(...)``.
"""
import api.config as cfg
import api.models as models
import api.routes as routes
monkeypatch.setattr(routes, 'require', lambda body, *keys: None)
monkeypatch.setattr(
routes,
'bad',
lambda _handler, msg, status=400: {'ok': False, 'error': msg, 'status': status},
)
monkeypatch.setattr(
routes,
'j',
lambda _handler, payload, status=200, extra_headers=None: payload,
)
monkeypatch.setattr(
routes,
'_persist_handoff_summary',
lambda sid, summary, channel, rounds, fallback=False: persisted.append(
{
'sid': sid,
'summary': summary,
'channel': channel,
'rounds': rounds,
'fallback': fallback,
}
)
or {'ok': True},
)
monkeypatch.setattr(
models,
'count_conversation_rounds',
lambda sid, since=None: models.CONVERSATION_ROUND_THRESHOLD,
)
monkeypatch.setattr(
models,
'get_cli_session_messages',
lambda sid: [
{'role': 'user', 'content': 'What remains to do?', 'timestamp': 1.0},
{'role': 'assistant', 'content': 'One review step remains.', 'timestamp': 2.0},
{'role': 'user', 'content': 'And after that?', 'timestamp': 3.0},
{'role': 'assistant', 'content': 'Then we ship.', 'timestamp': 4.0},
],
)
monkeypatch.setattr(
cfg,
'resolve_model_provider',
lambda resolved_model=None: (
'gpt-test',
'openai-codex',
'https://chatgpt.com/backend-api/codex',
),
)
persisted = []
transport = _FakeTransport(SUMMARY_TEXT)
class _CodexAgent:
api_mode = 'codex_responses'
def __init__(self, *args, **kwargs):
self.model = kwargs.get('model')
self.provider = kwargs.get('provider')
self.base_url = kwargs.get('base_url')
self.reasoning_config = None
def _build_api_kwargs(self, api_messages):
return {'model': self.model, 'instructions': 'summary', 'input': [], 'store': False}
def _run_codex_stream(self, kwargs):
# Opaque response blob; normalizing is the transport's job on v0.21.
return object()
def _get_transport(self, mode=None):
assert mode == 'codex_responses'
return transport
def release_clients(self):
return None
# v0.21: the legacy method is gone from the agent.
assert not hasattr(_CodexAgent, '_normalize_codex_response')
fake_run_agent = types.ModuleType('run_agent')
fake_run_agent.AIAgent = _CodexAgent
monkeypatch.setitem(sys.modules, 'run_agent', fake_run_agent)
fake_runtime_module = types.ModuleType('hermes_cli.runtime_provider')
fake_runtime_module.resolve_runtime_provider = lambda requested=None: {
'api_key': 'x',
'provider': 'openai-codex',
'base_url': 'https://chatgpt.com/backend-api/codex',
}
fake_hermes_cli = types.ModuleType('hermes_cli')
fake_hermes_cli.__path__ = []
fake_hermes_cli.runtime_provider = fake_runtime_module
monkeypatch.setitem(sys.modules, 'hermes_cli', fake_hermes_cli)
monkeypatch.setitem(sys.modules, 'hermes_cli.runtime_provider', fake_runtime_module)
response = routes._handle_handoff_summary(
object(), {'session_id': 'session-codex-v021-transport'}
)
assert response['ok'] is True
assert response['fallback'] is False
assert response['summary'] == SUMMARY_TEXT
assert persisted, 'handoff summary must be persisted'
assert persisted[0]['summary'] == SUMMARY_TEXT
assert persisted[0]['fallback'] is False