Skip to content

Commit 2b2c269

Browse files
mrveisstclaude
authored
refactor(tests): extract canonical make_llm_response fixture (#7134) (#7139)
Promotes the existing private ``_build_mock_response`` in ``tests/fixtures/mocks.py`` to a public, fully-parameterized ``make_llm_response`` factory. The migration tests I authored across PRs #7101 / #7118 / #7131 each rolled their own LLMResponse stub — three different patterns for the same shape. Combined with 4 pre-existing patterns elsewhere, that's 7 ad-hoc mock styles. Why this matters ---------------- The factory's primary value: returns the **real** ``LLMResponse`` from ``llm_interface_pkg.models`` when importable. If a future field rename or type change happens on that dataclass, every test that uses the factory fails at fixture-construction — instead of silently producing wrong-shape mocks the way ``MagicMock(content=...)`` would. This is the class of drift documented in ``feedback_verify_return_shape_in_method_migration.md`` (#7069 lesson). Changes ------- - ``tests/fixtures/mocks.py``: promoted ``_build_mock_response`` → public ``make_llm_response`` with keyword-only args (``content``, ``error``, ``model``, ``provider``). Underscore alias retained as a back-compat one-liner — no callers break. - ``tests/fixtures/__init__.py``: re-exported. - ``tests/fixtures/test_make_llm_response.py``: 7 contract tests pinning the factory's behavior — most importantly the ``isinstance(LLMResponse)`` check that catches future drift. - Migrated my 2 in-repo callers off ``class _StubResponse``: - api/chat_generate_ai_response_test.py - orchestration/workflow_documentation_summary_test.py Both now import ``from tests.fixtures import make_llm_response`` and wrap it in a same-name pytest fixture so the existing test code doesn't need rewriting. Out of scope (deliberately not in this PR) ------------------------------------------ The 4 pre-existing ad-hoc patterns (each has different historical reasons — keeping the migration surface small): - tests/orchestration/test_subagent_dispatcher_reflection.py:31 (``_make_llm_response`` factory) - tests/api/test_knowledge_grounding.py (5 inline ``MagicMock(content=...)``) These can migrate incrementally; the canonical helper is now available when someone touches those files. PR #7131 (perf benchmarks, still open) deliberately not touched — amending an open PR mid-flight invites conflicts. It can migrate after this lands. Verification ------------ $ python3 -m pytest \ tests/fixtures/test_make_llm_response.py \ api/chat_generate_ai_response_test.py \ orchestration/workflow_documentation_summary_test.py -xvs ============================== 19 passed in 2.58s =============================== - 7 new contract tests (factory behavior pinned) - 5 existing api/chat tests still green via canonical fixture - 7 existing workflow_documentation tests still green via canonical fixture Closes #7134 Co-authored-by: t <t@t> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 01cf706 commit 2b2c269

5 files changed

Lines changed: 155 additions & 20 deletions

File tree

‎autobot-backend/api/chat_generate_ai_response_test.py‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,15 @@
2626

2727
import pytest
2828

29+
from tests.fixtures import make_llm_response as _make_llm_response_factory
30+
2931

3032
@pytest.fixture
3133
def make_llm_response():
32-
"""Return a factory that builds a stub ``LLMResponse``-shaped object."""
33-
34-
class _StubResponse:
35-
def __init__(self, *, content: str = "", error: str | None = None):
36-
self.content = content
37-
self.error = error
38-
39-
return _StubResponse
34+
"""Canonical LLMResponse factory (#7134) — wraps the shared
35+
``tests.fixtures.make_llm_response`` so each test reads the same shape.
36+
"""
37+
return _make_llm_response_factory
4038

4139

4240
@pytest.mark.asyncio

‎autobot-backend/orchestration/workflow_documentation_summary_test.py‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,16 @@
2828

2929
from orchestration.types import DocumentationType, WorkflowDocumentation
3030
from orchestration.workflow_documentation import WorkflowDocumenter
31+
from tests.fixtures import make_llm_response as _make_llm_response_factory
3132

3233

3334
@pytest.fixture
3435
def make_llm_response():
35-
"""Return a factory that builds a stub ``LLMResponse``-shaped object."""
36-
37-
class _StubResponse:
38-
def __init__(self, *, content: str = "", error: str | None = None):
39-
self.content = content
40-
self.error = error
41-
42-
return _StubResponse
36+
"""Canonical LLMResponse factory (#7134) — wraps the shared
37+
``tests.fixtures.make_llm_response`` so the test sees the real
38+
LLMResponse field shape, not a hand-rolled stub that could drift.
39+
"""
40+
return _make_llm_response_factory
4341

4442

4543
def _make_workflow_doc(workflow_id: str = "wf-1") -> WorkflowDocumentation:

‎autobot-backend/tests/fixtures/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
MockLLMInterface,
1616
MockLLMService,
1717
MockWorkerNode,
18+
make_llm_response,
1819
)
1920

2021
__all__ = [
@@ -23,4 +24,5 @@
2324
"MockLLMInterface",
2425
"MockLLMService",
2526
"MockWorkerNode",
27+
"make_llm_response",
2628
]

‎autobot-backend/tests/fixtures/mocks.py‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,52 @@ class _MockLLMResponseShim:
8585
error: Optional[str] = None
8686

8787

88-
def _build_mock_response(content: str):
89-
"""Return the real `LLMResponse` if importable, else a duck-typed shim."""
88+
def make_llm_response(
89+
*,
90+
content: str = "",
91+
error: Optional[str] = None,
92+
model: str = "mock",
93+
provider: str = "mock",
94+
):
95+
"""Build an LLMResponse-shaped value for tests (canonical, #7134).
96+
97+
Returns the real ``LLMResponse`` from ``llm_interface_pkg.models`` when
98+
importable — that pins the field contract, so a future field rename or
99+
type change breaks the fixture (and every test that uses it) at import
100+
time rather than silently producing wrong-shape mocks. Falls back to a
101+
duck-typed shim when the real module isn't available (e.g. during
102+
`tests/fixtures/` collection in a stripped-down environment).
103+
104+
All arguments are keyword-only — positional args would invite the same
105+
field-shape drift the lesson in
106+
``feedback_verify_return_shape_in_method_migration.md`` is about.
107+
108+
Replaces the 7+ ad-hoc patterns surfaced in #7134:
109+
- ``_StubResponse`` / ``_MockLLMResponseShim`` private classes
110+
- ``MagicMock(content=..., error=None)`` inline forms
111+
- ``_make_llm_response(content)`` factories
112+
- ``MagicMock(content=...)`` without explicit error= field
113+
114+
Args:
115+
content: ``LLMResponse.content`` value.
116+
error: ``LLMResponse.error`` value (None for healthy responses).
117+
model: provider model name; defaults to ``"mock"``.
118+
provider: provider name; defaults to ``"mock"``.
119+
"""
90120
try:
91121
from llm_interface_pkg.models import LLMResponse
92122

93-
return LLMResponse(content=content, model="mock", provider="mock")
123+
return LLMResponse(content=content, error=error, model=model, provider=provider)
94124
except Exception:
95-
return _MockLLMResponseShim(content=content)
125+
return _MockLLMResponseShim(
126+
content=content, error=error, model=model, provider=provider
127+
)
128+
129+
130+
# Back-compat alias — internal callers still reference the underscore name.
131+
# New code should use `make_llm_response` directly.
132+
def _build_mock_response(content: str):
133+
return make_llm_response(content=content)
96134

97135

98136
class MockLLMService:
@@ -285,4 +323,5 @@ def reset(self) -> None:
285323
"MockCommandValidator",
286324
"MockKnowledgeBase",
287325
"MockWorkerNode",
326+
"make_llm_response",
288327
]
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# AutoBot - AI-Powered Automation Platform
2+
# Copyright (c) 2025 mrveiss
3+
# Author: mrveiss
4+
"""Contract tests for ``tests.fixtures.mocks.make_llm_response`` (#7134).
5+
6+
Pins the canonical LLMResponse-stub factory's behavior so the 7+ ad-hoc
7+
test patterns that exist today can be migrated onto it with confidence.
8+
9+
Why a fixture-test exists at all: the factory's whole value comes from
10+
returning the **real** ``LLMResponse`` from ``llm_interface_pkg.models``
11+
when importable. If a future field rename or type change happens on
12+
that dataclass, this test fails first — the migration target is broken,
13+
not silently producing wrong-shape mocks.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import pytest
19+
20+
from tests.fixtures import make_llm_response
21+
22+
23+
def test_returns_real_llmresponse_when_importable() -> None:
24+
"""The factory's primary value: returns the actual LLMResponse class
25+
so the field contract is pinned. If imports work in this environment,
26+
the result must be an instance of the real LLMResponse.
27+
"""
28+
try:
29+
from llm_interface_pkg.models import LLMResponse
30+
except ImportError:
31+
pytest.skip("llm_interface_pkg.models not importable — fallback path is fine")
32+
33+
response = make_llm_response(content="hello")
34+
assert isinstance(response, LLMResponse)
35+
36+
37+
def test_default_content_is_empty_string() -> None:
38+
"""Bare call returns a healthy, no-content response."""
39+
response = make_llm_response()
40+
assert response.content == ""
41+
assert response.error is None
42+
43+
44+
def test_passes_through_all_documented_kwargs() -> None:
45+
response = make_llm_response(
46+
content="hello",
47+
error="rate limit",
48+
model="gpt-4",
49+
provider="openai",
50+
)
51+
assert response.content == "hello"
52+
assert response.error == "rate limit"
53+
assert response.model == "gpt-4"
54+
assert response.provider == "openai"
55+
56+
57+
def test_keyword_only_args() -> None:
58+
"""All args are keyword-only — positional would invite the same
59+
field-shape drift the function exists to prevent.
60+
"""
61+
with pytest.raises(TypeError):
62+
make_llm_response("hello") # type: ignore[misc]
63+
64+
65+
def test_error_field_supports_none_default() -> None:
66+
"""The healthy default — ``error=None`` — must not be coerced to ``""``;
67+
callers like ``WorkflowDocumenter`` check ``if not response.error`` and
68+
an empty-string error would be indistinguishable from None for falsy
69+
checks but DIFFERENT from None for ``response.error is None`` checks.
70+
"""
71+
response = make_llm_response(content="ok")
72+
assert response.error is None
73+
# Falsy check works either way:
74+
assert not response.error
75+
76+
77+
def test_back_compat_underscore_alias_still_works() -> None:
78+
"""``_build_mock_response(content)`` is the legacy entry point used
79+
inside ``MockLLMService.chat`` — keeping it functional avoids breaking
80+
the existing demo `__main__` blocks.
81+
"""
82+
from tests.fixtures.mocks import _build_mock_response
83+
84+
response = _build_mock_response("hello")
85+
assert response.content == "hello"
86+
# Legacy alias still uses the documented "mock" defaults
87+
assert response.model == "mock"
88+
assert response.provider == "mock"
89+
90+
91+
def test_factory_is_re_exported_from_fixtures_package() -> None:
92+
"""``from tests.fixtures import make_llm_response`` is the documented
93+
import path — pinning it prevents an accidental rename / removal.
94+
"""
95+
import tests.fixtures as fixtures
96+
97+
assert "make_llm_response" in fixtures.__all__
98+
assert callable(fixtures.make_llm_response)

0 commit comments

Comments
 (0)