Skip to content

Commit 81a2cfe

Browse files
declan-scaleclaude
andcommitted
test(harness): extract shared FakeSpan/FakeTracing + parametrize determinism test once
Adds tests/lib/core/harness/_fakes.py with a single superset FakeSpan/FakeTracing (started/ended/ended_spans plus started_names/started_pairs/ended_outputs views) and migrates every consumer off its local copy. Keeps the conformance determinism test once (parametrized over all_fixtures) and drops the per-harness copies. run_yield_turn and test_langgraph_sync_unified's _FakeTracingBackend left in place (genuinely divergent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ee8aa4c commit 81a2cfe

14 files changed

Lines changed: 110 additions & 288 deletions

tests/lib/adk/test_pydantic_ai_sync_unified.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626

2727
from agentex.lib.core.harness import UnifiedEmitter
28+
from tests.lib.core.harness._fakes import FakeTracing
2829
from agentex.lib.adk._modules._pydantic_ai_turn import PydanticAITurn
2930

3031

@@ -37,25 +38,6 @@ async def _collect(stream: AsyncIterator[Any]) -> list[Any]:
3738
return [e async for e in stream]
3839

3940

40-
class _FakeSpan:
41-
def __init__(self, name: str):
42-
self.name = name
43-
self.output: Any = None
44-
45-
46-
class _FakeTracing:
47-
def __init__(self) -> None:
48-
self.started: list[tuple[str, str | None, Any]] = []
49-
self.ended: list[tuple[str, Any]] = []
50-
51-
async def start_span(self, *, trace_id, name, input=None, parent_id=None, data=None, task_id=None):
52-
self.started.append((name, parent_id, input))
53-
return _FakeSpan(name)
54-
55-
async def end_span(self, *, trace_id, span):
56-
self.ended.append((span.name, span.output))
57-
58-
5941
def _make_result_event(usage: RunUsage | None = None) -> AgentRunResultEvent:
6042
result = AgentRunResult(output="done", _output_tool_name=None)
6143
if usage is not None:
@@ -129,7 +111,7 @@ async def test_tool_span_opened_and_closed(self):
129111
),
130112
]
131113

132-
fake = _FakeTracing()
114+
fake = FakeTracing()
133115
turn = PydanticAITurn(_aiter(tool_events), model="openai:gpt-4o")
134116
emitter = UnifiedEmitter(task_id="t", trace_id="tr", parent_span_id="p", tracing=fake)
135117

@@ -152,7 +134,7 @@ async def test_reasoning_span_opened_and_closed(self):
152134
PartEndEvent(index=0, part=ThinkingPart(content="let me think")),
153135
]
154136

155-
fake = _FakeTracing()
137+
fake = FakeTracing()
156138
turn = PydanticAITurn(_aiter(reasoning_events), model="openai:gpt-4o")
157139
emitter = UnifiedEmitter(task_id="t", trace_id="tr", parent_span_id="p", tracing=fake)
158140

@@ -177,7 +159,7 @@ async def test_no_trace_id_means_no_spans(self):
177159
),
178160
]
179161

180-
fake = _FakeTracing()
162+
fake = FakeTracing()
181163
turn = PydanticAITurn(_aiter(raw_events), model="openai:gpt-4o")
182164
emitter = UnifiedEmitter(task_id="t", trace_id=None, parent_span_id=None, tracing=fake)
183165

@@ -199,7 +181,7 @@ async def test_tracer_false_suppresses_spans_even_with_trace_id(self):
199181
),
200182
]
201183

202-
fake = _FakeTracing()
184+
fake = FakeTracing()
203185
turn = PydanticAITurn(_aiter(raw_events), model="openai:gpt-4o")
204186
emitter = UnifiedEmitter(task_id="t", trace_id="tr", parent_span_id="p", tracer=False, tracing=fake)
205187

tests/lib/core/harness/_fakes.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Shared test doubles for the unified harness test suites.
2+
3+
A single superset implementation of the in-memory tracing backend used across
4+
the harness tests. Three recording shapes were previously duplicated:
5+
6+
- Shape-1 (richest): ``started`` = ``(name, parent_id, input)`` 3-tuples,
7+
``ended`` = ``(name, output)`` 2-tuples, plus an ``ended_spans`` list of the
8+
closed ``FakeSpan`` objects (which carry ``.name``, ``.output``, ``.data``).
9+
- Shape-2: ``started`` = ``(name, parent_id)`` 2-tuples, ``ended`` =
10+
``(name, output)``.
11+
- Shape-3: ``started`` = bare names, ``ended`` = bare outputs.
12+
13+
``FakeTracing`` records the richest (shape-1) form and exposes read-only
14+
convenience properties (``started_names``, ``started_pairs``,
15+
``ended_outputs``) so shape-2 and shape-3 assertions stay clean.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from typing import Any
21+
22+
23+
class FakeSpan:
24+
def __init__(self, name: str) -> None:
25+
self.name = name
26+
self.output: Any = None
27+
self.data: Any = None
28+
29+
30+
class FakeTracing:
31+
def __init__(self) -> None:
32+
self.started: list[tuple[str, Any, Any]] = []
33+
self.ended: list[tuple[str, Any]] = []
34+
self.ended_spans: list[FakeSpan] = []
35+
36+
async def start_span(
37+
self,
38+
*,
39+
trace_id: str,
40+
name: str,
41+
input: Any = None,
42+
parent_id: Any = None,
43+
data: Any = None,
44+
task_id: Any = None,
45+
) -> FakeSpan:
46+
self.started.append((name, parent_id, input))
47+
return FakeSpan(name)
48+
49+
async def end_span(self, *, trace_id: str, span: FakeSpan) -> None:
50+
self.ended.append((span.name, span.output))
51+
self.ended_spans.append(span)
52+
53+
@property
54+
def started_names(self) -> list[str]:
55+
return [name for (name, _parent, _input) in self.started]
56+
57+
@property
58+
def started_pairs(self) -> list[tuple[str, Any]]:
59+
return [(name, parent) for (name, parent, _input) in self.started]
60+
61+
@property
62+
def ended_outputs(self) -> list[Any]:
63+
return [output for (_name, output) in self.ended]

tests/lib/core/harness/conformance/runner.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@
6464
from __future__ import annotations
6565

6666
import json
67-
import types as _types
6867
from typing import Any, NamedTuple, override
6968
from dataclasses import dataclass
7069

7170
from agentex.types.text_delta import TextDelta
7271
from agentex.types.task_message import TaskMessage
72+
from tests.lib.core.harness._fakes import FakeTracing
7373
from agentex.lib.core.harness.types import SpanSignal, StreamTaskMessage
7474
from agentex.lib.core.harness.tracer import SpanTracer
7575
from agentex.types.task_message_update import (
@@ -296,30 +296,6 @@ def streaming_task_message_context(
296296
return _FakeCtx(self.sink, ctype, initial_content)
297297

298298

299-
class _FakeTracing:
300-
"""Minimal tracing backend: records started/ended span names + outputs."""
301-
302-
def __init__(self) -> None:
303-
self.started: list[str] = []
304-
self.ended: list[Any] = []
305-
306-
async def start_span(
307-
self,
308-
*,
309-
trace_id: str,
310-
name: str,
311-
input: Any = None,
312-
parent_id: Any = None,
313-
data: Any = None,
314-
task_id: Any = None,
315-
) -> Any:
316-
self.started.append(name)
317-
return _types.SimpleNamespace()
318-
319-
async def end_span(self, *, trace_id: str, span: Any) -> None:
320-
self.ended.append(getattr(span, "output", None))
321-
322-
323299
class _RecordingTracer(SpanTracer):
324300
"""SpanTracer that records every SpanSignal it actually receives.
325301
@@ -486,7 +462,7 @@ async def run_cross_channel_conformance(
486462
from agentex.lib.core.harness.yield_delivery import yield_events
487463

488464
# --- yield channel ---
489-
tracer_yield = _RecordingTracer(tracing=_FakeTracing())
465+
tracer_yield = _RecordingTracer(tracing=FakeTracing())
490466
yield_out = [e async for e in yield_events(_gen(fixture.events), tracer=tracer_yield)]
491467

492468
# Span signals the yield channel actually emitted to its tracer
@@ -496,7 +472,7 @@ async def run_cross_channel_conformance(
496472
yield_deliveries = _yield_text_reasoning_seq(_yield_logical_deliveries(yield_out))
497473

498474
# --- auto_send channel ---
499-
tracer_auto = _RecordingTracer(tracing=_FakeTracing())
475+
tracer_auto = _RecordingTracer(tracing=FakeTracing())
500476
fake_streaming = _FakeStreaming()
501477
await auto_send(
502478
_gen(fixture.events),

tests/lib/core/harness/conformance/test_codex_conformance.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from agentex.lib.core.harness.types import StreamTaskMessage
2020
from agentex.lib.adk._modules._codex_sync import convert_codex_to_agentex_events
2121

22-
from .runner import Fixture, register, derive_all
22+
from .runner import Fixture, register
2323

2424

2525
async def _aiter(items: list[Any]) -> AsyncIterator[Any]:
@@ -208,17 +208,6 @@ def _build(events: list[Any]) -> list[StreamTaskMessage]:
208208
_LOCAL_FIXTURES = [_CODEX_TEXT, _CODEX_TOOL, _CODEX_REASONING, _CODEX_MULTI]
209209

210210

211-
@pytest.mark.parametrize("fixture", _LOCAL_FIXTURES, ids=lambda f: f.name)
212-
def test_codex_span_derivation_is_deterministic(fixture: Fixture) -> None:
213-
"""Span derivation over codex events is deterministic (cross-channel guarantee).
214-
215-
Deriving twice over the same events yields identical signals. This is the
216-
invariant that makes ``yield`` and ``auto_send`` delivery equivalent: both
217-
observe the same event stream, so their tracing side effects are identical.
218-
"""
219-
assert derive_all(fixture.events) == derive_all(fixture.events)
220-
221-
222211
@pytest.mark.parametrize("fixture", _LOCAL_FIXTURES, ids=lambda f: f.name)
223212
def test_codex_events_are_non_empty(fixture: Fixture) -> None:
224213
"""Every codex fixture yields at least one StreamTaskMessage*."""

tests/lib/core/harness/conformance/test_langgraph_conformance.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from agentex.types.tool_response_content import ToolResponseContent
3333
from agentex.types.reasoning_content_delta import ReasoningContentDelta
3434

35-
from .runner import Fixture, register, derive_all, run_cross_channel_conformance
35+
from .runner import Fixture, register, run_cross_channel_conformance
3636

3737
# ---------------------------------------------------------------------------
3838
# Fixtures
@@ -216,14 +216,3 @@ async def test_cross_channel_equivalence(fixture: Fixture) -> None:
216216
assert yield_spans == auto_spans, (
217217
f"[{fixture.name}] span signals differ:\n yield: {yield_spans}\n auto_send: {auto_spans}"
218218
)
219-
220-
221-
# ---------------------------------------------------------------------------
222-
# Backward-compatible determinism guard
223-
# ---------------------------------------------------------------------------
224-
225-
226-
@pytest.mark.parametrize("fixture", _LANGGRAPH_FIXTURES, ids=lambda f: f.name)
227-
def test_span_derivation_is_deterministic(fixture: Fixture) -> None:
228-
"""Span derivation over the same event list is idempotent."""
229-
assert derive_all(fixture.events) == derive_all(fixture.events)

tests/lib/core/harness/conformance/test_pydantic_ai_conformance.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
from .runner import (
4040
Fixture,
4141
register,
42-
derive_all,
4342
run_cross_channel_conformance,
4443
)
4544

@@ -181,14 +180,3 @@ async def test_cross_channel_equivalence(fixture: Fixture) -> None:
181180
assert yield_spans == auto_spans, (
182181
f"[{fixture.name}] span signals differ:\n yield: {yield_spans}\n auto_send: {auto_spans}"
183182
)
184-
185-
186-
# ---------------------------------------------------------------------------
187-
# Backward-compatible determinism guard
188-
# ---------------------------------------------------------------------------
189-
190-
191-
@pytest.mark.parametrize("fixture", _FIXTURES, ids=lambda f: f.name)
192-
def test_span_derivation_is_deterministic(fixture: Fixture) -> None:
193-
"""Span derivation over the same event list is idempotent."""
194-
assert derive_all(fixture.events) == derive_all(fixture.events)

tests/lib/core/harness/test_auto_send.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
This mirrors _langgraph_async.py lines 62-78 and 100-127.
1010
"""
1111

12-
import types as _types
1312
from datetime import datetime
1413

1514
import pytest
1615

1716
from agentex.types.task_message import TaskMessage
1817
from agentex.types.text_content import TextContent
18+
from tests.lib.core.harness._fakes import FakeTracing
1919
from agentex.lib.core.harness.tracer import SpanTracer
2020
from agentex.types.task_message_delta import TextDelta
2121
from agentex.types.tool_request_delta import ToolRequestDelta
@@ -181,21 +181,9 @@ async def test_auto_send_posts_full_tool_messages():
181181
# ---------------------------------------------------------------------------
182182

183183

184-
class _RecordTracing:
185-
def __init__(self):
186-
self.started, self.ended = [], []
187-
188-
async def start_span(self, *, trace_id, name, input=None, parent_id=None, data=None, task_id=None):
189-
self.started.append(name)
190-
return _types.SimpleNamespace()
191-
192-
async def end_span(self, *, trace_id, span):
193-
self.ended.append(getattr(span, "output", None))
194-
195-
196184
@pytest.mark.asyncio
197185
async def test_auto_send_derives_tool_spans_via_tracer():
198-
fake_tracing = _RecordTracing()
186+
fake_tracing = FakeTracing()
199187
tracer = SpanTracer(trace_id="t", parent_span_id="p", tracing=fake_tracing)
200188
streaming = _FakeStreaming()
201189

@@ -228,8 +216,8 @@ async def test_auto_send_derives_tool_spans_via_tracer():
228216
result = await auto_send(_gen(events), task_id="task1", tracer=tracer, streaming=streaming)
229217

230218
assert result.final_text == ""
231-
assert fake_tracing.started == ["Bash"]
232-
assert fake_tracing.ended == ["ok"]
219+
assert fake_tracing.started_names == ["Bash"]
220+
assert fake_tracing.ended_outputs == ["ok"]
233221

234222

235223
# ---------------------------------------------------------------------------

tests/lib/core/harness/test_emitter.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from agentex.types.task_message import TaskMessage
44
from agentex.types.text_content import TextContent
5+
from tests.lib.core.harness._fakes import FakeTracing
56
from agentex.lib.core.harness.types import TurnUsage
67
from agentex.lib.core.harness.emitter import UnifiedEmitter
78
from agentex.types.task_message_delta import TextDelta
@@ -12,14 +13,6 @@
1213
)
1314

1415

15-
class _FakeTracing:
16-
async def start_span(self, **kw):
17-
return None
18-
19-
async def end_span(self, **kw):
20-
pass
21-
22-
2316
class _FakeCtx:
2417
"""Minimal StreamingTaskMessageContext fake (see test_auto_send.py)."""
2518

@@ -84,7 +77,7 @@ async def test_emitter_yield_mode_passes_through():
8477
async def test_emitter_tracing_default_on_when_trace_id_present():
8578
# Inject a fake tracing backend so the test env doesn't need temporalio.
8679
# This exercises the default-on path (tracer=None) when trace_id is truthy.
87-
emitter = UnifiedEmitter(task_id="t", trace_id="trace1", parent_span_id="p", tracing=_FakeTracing())
80+
emitter = UnifiedEmitter(task_id="t", trace_id="trace1", parent_span_id="p", tracing=FakeTracing())
8881
assert emitter.tracer is not None
8982

9083

0 commit comments

Comments
 (0)