Skip to content

Commit d2cc2a9

Browse files
Address PR 85 review: header tweak + two coverage tests
Updates the module-header comment in tests/conformance/test_observability_langfuse.py to clarify that the harness *infrastructure* supports the 031/032/033 topology shapes while activation is currently deferred. Drops the forward-reference to fixture 027 in an internal comment that referenced fixtures this module doesn't currently run. Adds two focused unit tests for the observer behavior changes that were only exercised by deferred conformance fixtures: - test_entry_node_resolves_to_wrapper_when_entry_is_subgraph pins the entry_node correctness fix: when the outer entry IS a SubgraphNode, the first event arrives from inside the subgraph (event.namespace = (wrapper, inner), node_name = inner). Both trace.name and trace.metadata.entry_node must resolve to the wrapper, not the inner node. - test_detached_subgraph_subgraph_name_placement pins the audit fix from coord-thread msg 07: in detached mode the wrapper role migrates to the detached trace. The parent trace's link observation must not carry subgraph_name; the detached trace's dispatch observation must. Both tests sit alongside the existing dispatch-synthesis tests.
1 parent a8d1552 commit d2cc2a9

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

tests/conformance/test_observability_langfuse.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
# Spec mapping (observability §8): drives the Langfuse mapping fixtures
2-
# (022/023/024 basic/generation/prompt-linkage, plus 031/032/033 graph
3-
# topology) against the in-memory LangfuseObserver client. Sibling of
4-
# test_observability.py (OTel mapping); shares no harness state with
5-
# the OTel side — each fixture builds its own graph + observer instance.
1+
# Spec mapping (observability §8): drives the Langfuse mapping
2+
# fixtures (022 basic-trace, 023 generation-rendering, 024
3+
# prompt-linkage) against the in-memory LangfuseObserver client.
4+
# Sibling of test_observability.py (OTel mapping); shares no harness
5+
# state with the OTel side — each fixture builds its own graph +
6+
# observer instance.
7+
#
8+
# The harness also supports the graph-topology shapes used by
9+
# 031/032/033 (subgraph / fan-out / detached-trace) via the
10+
# cross-capability adapter.build_graph helper, but activation of
11+
# those three fixtures is currently deferred — see the
12+
# `_LANGFUSE_FIXTURES` frozenset comment for the gating questions.
613

714
"""Run spec observability Langfuse conformance fixtures."""
815

@@ -245,7 +252,7 @@ async def _run_case(case: Mapping[str, Any]) -> None:
245252
# ---- Graph build
246253
# Two paths: topology fixtures (031/032/033) need the full
247254
# ``adapter.build_graph`` machinery for subgraph / fan_out shapes;
248-
# LLM/prompt fixtures (022/023/024/027) use the simpler hand-rolled
255+
# LLM/prompt fixtures (022/023/024) use the simpler hand-rolled
249256
# per-node build that knows about ``calls_llm`` / ``renders_prompt``.
250257
if _has_topology_constructs(case):
251258
subgraphs = _compile_subgraphs(case)

tests/unit/test_observability_langfuse.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,32 @@ def _find_observation(trace: LangfuseTrace, name: str) -> LangfuseObservation:
196196
raise AssertionError(f"observation {name!r} not in trace {trace.id!r}")
197197

198198

199+
async def test_entry_node_resolves_to_wrapper_when_entry_is_subgraph() -> None:
200+
# When the outer entry IS a SubgraphNode, the first event the
201+
# observer sees comes from inside the subgraph
202+
# (event.namespace = (wrapper, inner), event.node_name = inner).
203+
# `entry_node` and trace.name MUST resolve to the wrapper node
204+
# name (event.namespace[0]), not the inner node name.
205+
inner = (
206+
GraphBuilder(_S)
207+
.add_node("inner_a", lambda _s: _record("inner_a"))
208+
.add_edge("inner_a", END)
209+
.set_entry("inner_a")
210+
.compile()
211+
)
212+
parent = GraphBuilder(_S).add_subgraph_node("sub", inner).add_edge("sub", END).set_entry("sub").compile()
213+
graph, client, _ = _attach(parent)
214+
215+
await graph.invoke(_S())
216+
await graph.drain()
217+
218+
trace = next(iter(client.traces.values()))
219+
assert trace.name == "sub", f"trace name should be the wrapper, got {trace.name!r}"
220+
assert trace.metadata.get("entry_node") == "sub", (
221+
f"entry_node should be the wrapper, got {trace.metadata.get('entry_node')!r}"
222+
)
223+
224+
199225
async def test_subgraph_dispatch_observation_parents_inner_node() -> None:
200226
inner = (
201227
GraphBuilder(_S)
@@ -294,6 +320,40 @@ async def test_detached_subgraph_opens_separate_trace() -> None:
294320
assert inner_node.parent_observation_id == detached_dispatch.id
295321

296322

323+
async def test_detached_subgraph_subgraph_name_placement() -> None:
324+
# Per coord thread `discuss-observability-langfuse-mapping` msg 07
325+
# and the wrapper-role-migration framing: in detached mode the
326+
# wrapper role migrates to the detached trace. The parent trace's
327+
# link observation IS the SubgraphNode span (no wrapper role) and
328+
# MUST NOT carry `subgraph_name`. The detached trace's dispatch
329+
# observation IS the migrated wrapper and MUST carry it.
330+
inner = (
331+
GraphBuilder(_S)
332+
.add_node("inner_a", lambda _s: _record("inner_a"))
333+
.add_edge("inner_a", END)
334+
.set_entry("inner_a")
335+
.compile()
336+
)
337+
parent = GraphBuilder(_S).add_subgraph_node("sub", inner).add_edge("sub", END).set_entry("sub").compile()
338+
graph, client, _ = _attach_with_detached(parent, detached_subgraphs=frozenset({"sub"}))
339+
340+
await graph.invoke(_S())
341+
await graph.drain()
342+
343+
main = next(t for t in client.traces.values() if "detached_from_invocation_id" not in t.metadata)
344+
detached = next(t for t in client.traces.values() if "detached_from_invocation_id" in t.metadata)
345+
346+
link_obs = _find_observation(main, "sub")
347+
assert "subgraph_name" not in link_obs.metadata, (
348+
f"link observation MUST NOT carry subgraph_name; got {link_obs.metadata!r}"
349+
)
350+
351+
detached_dispatch = _find_observation(detached, "sub")
352+
assert "subgraph_name" in detached_dispatch.metadata, (
353+
f"detached dispatch MUST carry subgraph_name; got {detached_dispatch.metadata!r}"
354+
)
355+
356+
297357
async def test_detached_fan_out_each_instance_gets_trace() -> None:
298358
async def _worker(_s: _WorkerState) -> Any:
299359
return {"result": "done"}

0 commit comments

Comments
 (0)