Skip to content

Commit 9d63896

Browse files
Harden Langfuse sibling matching and id assert
Fold in the python-side nuances from spec's Tier 2 review: - _assert_langfuse_observation_tree now disambiguates same-(type, name) sibling observations (032's per-instance "process" spans) by their scalar metadata rather than emission order, so the assertions can't bind the wrong sibling if the observer's emission order shifts. - _run_invocation_id_case now asserts the fixture's top-level verbatim invocation_id clause (035/036) against the in-memory recorder's raw trace.id, so it isn't half-asserted across the OTel and Langfuse runners.
1 parent abfcd13 commit 9d63896

1 file changed

Lines changed: 38 additions & 1 deletion

File tree

tests/conformance/test_observability.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2839,6 +2839,16 @@ async def _run_invocation_id_case(case: Mapping[str, Any]) -> None:
28392839
actual = trace.id
28402840
assert actual == val, f"trace.metadata.{key} {actual!r} != {val!r}"
28412841

2842+
# The fixture's top-level verbatim invocation_id clause (the §5.1
2843+
# caller_invocation_id_verbatim_on_attribute invariant): on the OTel side it
2844+
# is the openarmature.invocation_id span attribute; in the Langfuse runner
2845+
# the verbatim id surfaces as the in-memory recorder's raw trace.id.
2846+
expected_invocation_id = cast("dict[str, Any]", case["expected"]).get("invocation_id")
2847+
if expected_invocation_id is not None:
2848+
assert trace.id == expected_invocation_id, (
2849+
f"verbatim invocation_id: raw trace.id {trace.id!r} != {expected_invocation_id!r}"
2850+
)
2851+
28422852

28432853
async def _run_langfuse_generation_fixture(spec: Mapping[str, Any]) -> None:
28442854
"""Driver for the Langfuse Generation fixtures (023 generation rendering +
@@ -3867,6 +3877,23 @@ def _assert_langfuse_generation_fields(
38673877
)
38683878

38693879

3880+
def _obs_selection_matches(obs: Any, exp_metadata: Mapping[str, Any]) -> bool:
3881+
"""Read-only disambiguator for same-(type, name) sibling observations: an
3882+
actual is a candidate when its scalar expected-metadata values match.
3883+
3884+
Only scalars (str / int / float / bool) are used: placeholder tokens are
3885+
shared across siblings (correlation_id) so they don't disambiguate, and
3886+
running the value-matcher here would fire its binding side effects during
3887+
selection; sequences (namespace) are left to the value-matcher's list/tuple
3888+
handling. fan_out_index / step are the fields that actually distinguish.
3889+
"""
3890+
for key, val in exp_metadata.items():
3891+
is_placeholder = isinstance(val, str) and val.startswith("<") and val.endswith(">")
3892+
if isinstance(val, (str, int, float)) and not is_placeholder and obs.metadata.get(key) != val:
3893+
return False
3894+
return True
3895+
3896+
38703897
def _assert_langfuse_observation_tree(
38713898
trace: Any,
38723899
expected: list[dict[str, Any]],
@@ -3888,8 +3915,18 @@ def _assert_langfuse_observation_tree(
38883915
for exp in expected:
38893916
exp_type = cast("str", exp["type"])
38903917
exp_name = cast("str | None", exp.get("name"))
3918+
# Disambiguate same-(type, name) siblings (e.g. 032's per-instance
3919+
# "process" spans) by their scalar metadata, not list/emission order, so
3920+
# the assertions can't bind the wrong sibling if emission order shifts.
3921+
exp_meta = cast("dict[str, Any]", exp.get("metadata") or {})
38913922
match = next(
3892-
(o for o in remaining if o.type == exp_type and (exp_name is None or o.name == exp_name)),
3923+
(
3924+
o
3925+
for o in remaining
3926+
if o.type == exp_type
3927+
and (exp_name is None or o.name == exp_name)
3928+
and _obs_selection_matches(o, exp_meta)
3929+
),
38933930
None,
38943931
)
38953932
assert match is not None, (

0 commit comments

Comments
 (0)