Skip to content

Commit 504d056

Browse files
committed
Add unit tests for vcr_replay helper to lift patch coverage
32 in-memory tests covering the public surface of opencontractserver/utils/vcr_replay.py: - _normalize_body for None / empty str / bytes / str / dict / bytearray / fallback-coerced types, plus each volatile pattern (RUN_ID timestamp, Django document PK, OpenAI tool-call IDs, UUIDs). - _match_llm_body match + mismatch behavior including the volatility-stripping that lets a cassette recorded against one DB replay against another. Includes a guard test that real prompt differences still produce a mismatch (so the matcher does not drift toward over-permissiveness). - maybe_vcr_cassette env-var routing: bypass when unset / empty / 'off' / 'none-disabled', warn-and-bypass when cassette path is missing or the mode is unknown, yield a real cassette in record and replay modes. Tests stay strictly in-memory — no HTTP, no committed-cassette reads, no pydantic-ai. They run in well under a second.
1 parent 8e21942 commit 504d056

1 file changed

Lines changed: 293 additions & 0 deletions

File tree

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
"""
2+
Unit tests for ``opencontractserver.utils.vcr_replay``.
3+
4+
These tests stay strictly in-memory — they do NOT make any HTTP calls,
5+
read any cassette from disk, or invoke pydantic-ai. They cover the
6+
helper's public surface:
7+
8+
* ``_normalize_body`` for every input shape VCR may hand it (None,
9+
bytes, str, dict, other).
10+
* ``_match_llm_body`` mismatch / match behavior, including the
11+
volatility-stripping that lets a cassette recorded against one DB
12+
replay against another.
13+
* ``maybe_vcr_cassette`` env-var routing: bypass when unset / "off",
14+
bypass with a warning when ``OC_LLM_VCR_CASSETTE`` is missing,
15+
bypass with a warning on unknown mode, and the active path that
16+
actually constructs a vcr.VCR cassette.
17+
18+
The test for the active path uses a temporary cassette path so we don't
19+
touch the committed fixture cassette. ``maybe_vcr_cassette`` is a
20+
context manager; entering it with no recorded interactions and no
21+
network call means the cassette is a no-op for this test.
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import os
27+
import re
28+
import tempfile
29+
from contextlib import contextmanager
30+
from unittest import TestCase, mock
31+
32+
from opencontractserver.utils.vcr_replay import (
33+
_LLM_HOSTS,
34+
_VOLATILE_PATTERNS,
35+
_match_llm_body,
36+
_normalize_body,
37+
maybe_vcr_cassette,
38+
)
39+
40+
41+
@contextmanager
42+
def env_vars(**vars_to_set: str | None):
43+
"""Set / unset env vars within a ``with`` block, restoring afterward.
44+
45+
Pass ``None`` to ensure a variable is unset for the duration.
46+
"""
47+
saved: dict[str, str | None] = {}
48+
for k, v in vars_to_set.items():
49+
saved[k] = os.environ.get(k)
50+
if v is None:
51+
os.environ.pop(k, None)
52+
else:
53+
os.environ[k] = v
54+
try:
55+
yield
56+
finally:
57+
for k, prev in saved.items():
58+
if prev is None:
59+
os.environ.pop(k, None)
60+
else:
61+
os.environ[k] = prev
62+
63+
64+
class _FakeReq:
65+
"""Minimal stand-in for vcr.request.Request — only ``.body`` is read."""
66+
67+
def __init__(self, body):
68+
self.body = body
69+
70+
71+
class NormalizeBodyTests(TestCase):
72+
def test_none_returns_empty_bytes(self):
73+
self.assertEqual(_normalize_body(None), b"")
74+
75+
def test_empty_string_returns_empty_bytes(self):
76+
self.assertEqual(_normalize_body(""), b"")
77+
78+
def test_bytes_passthrough_when_no_volatile(self):
79+
body = b'{"foo":"bar"}'
80+
self.assertEqual(_normalize_body(body), body)
81+
82+
def test_string_input_is_encoded_then_normalized(self):
83+
body = '{"ts":1777000000000}'
84+
out = _normalize_body(body)
85+
self.assertIsInstance(out, bytes)
86+
# The 13-digit ms timestamp pattern should have been stripped.
87+
self.assertIn(b"<volatile>", out)
88+
self.assertNotIn(b"1777000000000", out)
89+
90+
def test_dict_input_is_serialized_deterministically(self):
91+
# Order should not affect the normalized output for the same
92+
# logical dict.
93+
a = _normalize_body({"a": 1, "b": 2})
94+
b = _normalize_body({"b": 2, "a": 1})
95+
self.assertEqual(a, b)
96+
self.assertIn(b'"a"', a)
97+
98+
def test_bytearray_input_is_handled(self):
99+
body = bytearray(b'{"x":1}')
100+
out = _normalize_body(body)
101+
self.assertEqual(out, b'{"x":1}')
102+
self.assertIsInstance(out, bytes)
103+
104+
def test_other_type_falls_back_to_repr(self):
105+
# Coercion of an int — covers the "last-resort" branch so the
106+
# matcher never raises on unexpected body types.
107+
out = _normalize_body(42)
108+
self.assertEqual(out, b"42")
109+
110+
def test_run_id_timestamp_is_stripped(self):
111+
body = b"before 1777504812606 after"
112+
out = _normalize_body(body)
113+
self.assertEqual(out, b"before <volatile> after")
114+
115+
def test_django_document_pk_is_stripped(self):
116+
body = b"document <user_content>foo</user_content> (ID: 56) extra"
117+
out = _normalize_body(body)
118+
self.assertIn(b"<volatile>", out)
119+
self.assertNotIn(b"(ID: 56)", out)
120+
121+
def test_openai_call_id_is_stripped(self):
122+
body = b'{"id":"call_GAVRUwuC2ZGQxMezJY7WDVHT","type":"function"}'
123+
out = _normalize_body(body)
124+
self.assertIn(b"<volatile>", out)
125+
self.assertNotIn(b"call_GAVRUwuC2ZGQxMezJY7WDVHT", out)
126+
127+
def test_tool_call_id_is_stripped(self):
128+
body = b'{"role":"tool","tool_call_id":"call_AmcD7e7RJpIBnDJu5F1Qjcvj"}'
129+
out = _normalize_body(body)
130+
self.assertIn(b"<volatile>", out)
131+
self.assertNotIn(b"call_AmcD7e7RJpIBnDJu5F1Qjcvj", out)
132+
133+
def test_uuid_is_stripped(self):
134+
body = b"annotation 12345678-1234-1234-1234-1234567890ab end"
135+
out = _normalize_body(body)
136+
self.assertEqual(out, b"annotation <volatile> end")
137+
138+
def test_compiled_volatile_patterns_are_bytes(self):
139+
# Defense-in-depth: make sure no string regex sneaked in. Bytes
140+
# patterns are required because the matcher always works on
141+
# bytes after normalization.
142+
for pat in _VOLATILE_PATTERNS:
143+
self.assertIsInstance(pat, re.Pattern)
144+
self.assertIsInstance(pat.pattern, bytes)
145+
146+
147+
class MatchLlmBodyTests(TestCase):
148+
def test_identical_bytes_match(self):
149+
a = _FakeReq(b'{"x":1}')
150+
b = _FakeReq(b'{"x":1}')
151+
# No raise == match. ``_match_llm_body`` is annotated -> None,
152+
# so we just call it and rely on absence of AssertionError.
153+
_match_llm_body(a, b)
154+
155+
def test_identical_after_volatility_strip(self):
156+
# Different RUN_ID timestamps but otherwise identical body.
157+
a = _FakeReq(b"prefix 1777504812606 suffix")
158+
b = _FakeReq(b"prefix 1777999999999 suffix")
159+
_match_llm_body(a, b)
160+
161+
def test_different_bodies_raise(self):
162+
a = _FakeReq(b"alpha")
163+
b = _FakeReq(b"beta")
164+
with self.assertRaises(AssertionError):
165+
_match_llm_body(a, b)
166+
167+
def test_string_body_matches_equivalent_bytes(self):
168+
# vcrpy may pass either, depending on the source of the request.
169+
a = _FakeReq(b"hello")
170+
b = _FakeReq("hello")
171+
_match_llm_body(a, b)
172+
173+
def test_none_body_matches_empty(self):
174+
a = _FakeReq(None)
175+
b = _FakeReq(b"")
176+
_match_llm_body(a, b)
177+
178+
def test_volatility_strip_works_across_call_id_changes(self):
179+
a = _FakeReq(b'{"tool_call_id":"call_AAAAAAAAA"}')
180+
b = _FakeReq(b'{"tool_call_id":"call_ZZZZZZZZZ"}')
181+
_match_llm_body(a, b)
182+
183+
def test_different_doc_ids_match_after_strip(self):
184+
a = _FakeReq(b"document foo (ID: 55) extra")
185+
b = _FakeReq(b"document foo (ID: 99) extra")
186+
_match_llm_body(a, b)
187+
188+
def test_real_substantive_difference_still_fails(self):
189+
# Past the volatility strip, real prompt differences must still
190+
# produce a mismatch. This guards against the matcher becoming
191+
# too permissive by accident.
192+
a = _FakeReq(b'{"prompt":"What is the title?"}')
193+
b = _FakeReq(b'{"prompt":"Summarize the document"}')
194+
with self.assertRaises(AssertionError):
195+
_match_llm_body(a, b)
196+
197+
198+
class MaybeVcrCassetteTests(TestCase):
199+
"""Env-var routing for ``maybe_vcr_cassette``.
200+
201+
Each test enters the context manager and asserts on the yielded
202+
value (None for the bypass paths, an actual cassette object on the
203+
active path).
204+
"""
205+
206+
def test_unset_mode_is_bypass(self):
207+
with env_vars(OC_LLM_VCR_MODE=None, OC_LLM_VCR_CASSETTE=None):
208+
with maybe_vcr_cassette() as ctx:
209+
self.assertIsNone(ctx)
210+
211+
def test_empty_mode_is_bypass(self):
212+
with env_vars(OC_LLM_VCR_MODE="", OC_LLM_VCR_CASSETTE="/tmp/x.yaml"):
213+
with maybe_vcr_cassette() as ctx:
214+
self.assertIsNone(ctx)
215+
216+
def test_off_mode_is_bypass(self):
217+
with env_vars(OC_LLM_VCR_MODE="off", OC_LLM_VCR_CASSETTE="/tmp/x.yaml"):
218+
with maybe_vcr_cassette() as ctx:
219+
self.assertIsNone(ctx)
220+
221+
def test_none_disabled_mode_is_bypass(self):
222+
# We use the special-case "none-disabled" name because plain
223+
# "none" collides with vcr's ``record_mode='none'`` (replay).
224+
with env_vars(
225+
OC_LLM_VCR_MODE="none-disabled", OC_LLM_VCR_CASSETTE="/tmp/x.yaml"
226+
):
227+
with maybe_vcr_cassette() as ctx:
228+
self.assertIsNone(ctx)
229+
230+
def test_record_mode_without_cassette_path_warns_and_bypasses(self):
231+
with env_vars(OC_LLM_VCR_MODE="record", OC_LLM_VCR_CASSETTE=None):
232+
with mock.patch(
233+
"opencontractserver.utils.vcr_replay.logger"
234+
) as mock_logger:
235+
with maybe_vcr_cassette() as ctx:
236+
self.assertIsNone(ctx)
237+
self.assertTrue(mock_logger.warning.called)
238+
239+
def test_unknown_mode_warns_and_bypasses(self):
240+
with env_vars(
241+
OC_LLM_VCR_MODE="oops",
242+
OC_LLM_VCR_CASSETTE="/tmp/x.yaml",
243+
):
244+
with mock.patch(
245+
"opencontractserver.utils.vcr_replay.logger"
246+
) as mock_logger:
247+
with maybe_vcr_cassette() as ctx:
248+
self.assertIsNone(ctx)
249+
self.assertTrue(mock_logger.warning.called)
250+
251+
def test_record_mode_yields_active_cassette(self):
252+
# The happy path: with both env vars set and a sensible mode,
253+
# we get a real cassette back. We use a tmpdir so we never
254+
# touch the committed fixture cassette.
255+
with tempfile.TemporaryDirectory() as td:
256+
cass_path = os.path.join(td, "test-cassette.yaml")
257+
with env_vars(
258+
OC_LLM_VCR_MODE="record",
259+
OC_LLM_VCR_CASSETTE=cass_path,
260+
):
261+
with maybe_vcr_cassette() as ctx:
262+
self.assertIsNotNone(ctx)
263+
# Cassette objects expose .requests as a sequence.
264+
self.assertTrue(hasattr(ctx, "requests"))
265+
266+
def test_replay_mode_yields_active_cassette_when_file_missing(self):
267+
# Replay mode against a missing cassette is technically valid —
268+
# the cassette is empty. Any subsequent request would raise, but
269+
# entering the context manager should succeed.
270+
with tempfile.TemporaryDirectory() as td:
271+
cass_path = os.path.join(td, "missing.yaml")
272+
with env_vars(
273+
OC_LLM_VCR_MODE="replay",
274+
OC_LLM_VCR_CASSETTE=cass_path,
275+
):
276+
with maybe_vcr_cassette() as ctx:
277+
self.assertIsNotNone(ctx)
278+
279+
280+
class LlmHostsTests(TestCase):
281+
"""Sanity check for the host allowlist."""
282+
283+
def test_openai_in_allowlist(self):
284+
self.assertIn("api.openai.com", _LLM_HOSTS)
285+
286+
def test_anthropic_in_allowlist(self):
287+
self.assertIn("api.anthropic.com", _LLM_HOSTS)
288+
289+
def test_localhost_not_in_allowlist(self):
290+
# We never want to intercept the embedder microservice or
291+
# internal localhost calls accidentally.
292+
self.assertNotIn("localhost", _LLM_HOSTS)
293+
self.assertNotIn("127.0.0.1", _LLM_HOSTS)

0 commit comments

Comments
 (0)