Skip to content

Commit 56b5bc9

Browse files
Guard against unaccounted observability fixtures
The conformance harness silently pytest.skip-ped any observability fixture not in _SUPPORTED_FIXTURES, so a future unwired spec fixture would not fail CI. Add test_observability_fixture_coverage_is_complete: every pinned fixture must be run or explicitly accounted for; the guard also catches stale entries and supported/not-run overlaps. Restructure the silent skips into three documented buckets: _DEFERRED_FIXTURES (future capability -- the embedding fixtures gated on proposal 0059, plus the nested-lineage case, whose stale "0045 not implemented" reason is corrected), _UNIT_TESTED_FIXTURES (32, each cited to its covering unit-suite file), and _CONVENTION_ONLY_FIXTURES (the 0048 section 9 queryable-observer pattern, doc-satisfied).
1 parent d8c3554 commit 56b5bc9

1 file changed

Lines changed: 178 additions & 13 deletions

File tree

tests/conformance/test_observability.py

Lines changed: 178 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,144 @@ def _reset_otel_global_tracer_provider(restore_to: object) -> None:
189189
)
190190

191191

192+
_EMBEDDING_DEFER = (
193+
"embedding capability (proposal 0059) unimplemented until v0.16.0; "
194+
"no embedding event/provider to record from"
195+
)
196+
197+
198+
# Pinned observability fixtures NOT run by this YAML harness, each with an
199+
# explicit reason. The coverage guard (test_observability_fixture_coverage_
200+
# is_complete) fails on any pinned fixture absent from _SUPPORTED_FIXTURES +
201+
# the three sets below, so a future unwired spec fixture cannot silently
202+
# pytest.skip past CI.
203+
#
204+
# _DEFERRED_FIXTURES — not run because the capability is unimplemented.
192205
_DEFERRED_FIXTURES: dict[str, str] = {
193-
# Proposal 0045 (nested-lineage augmentation, v0.37.0) — engine
194-
# + observer work lands in PR 11.
195-
"039-nested-lineage-augmentation": ("Proposal 0045 not yet implemented (PR 11)"),
196-
# Proposal 0067 (GenAI metrics, v0.68.0) — the embedding metrics
197-
# fixture sources from an embedding call, but the embedding capability
198-
# (proposal 0059, observability §5.5.8 / §5.5.9) is unimplemented in
199-
# python until v0.16.0, so there is no embedding event or provider to
200-
# record from. The LLM-path metric fixtures (088 / 090 / 091) run.
201-
"089-embedding-metrics-token-and-duration": (
202-
"Embedding capability (proposal 0059) unimplemented until v0.16.0"
206+
# Proposal 0045 IS implemented (v0.11.0), but the nested-case Langfuse
207+
# fixture stays deferred: it needs runtime-state item-list lookup for
208+
# nested fan-outs plus an augment_metadata_from_outer_item directive
209+
# the harness doesn't model yet.
210+
"039-nested-lineage-augmentation": (
211+
"nested-case Langfuse harness wiring not yet implemented (proposal 0045 nested fan-out)"
203212
),
213+
# Embedding observability (proposals 0059 / 0067 §11). The embedding
214+
# capability is unshipped until v0.16.0; the LLM-path equivalents run.
215+
**{
216+
fixture_id: _EMBEDDING_DEFER
217+
for fixture_id in (
218+
"074-embedding-event-dispatch",
219+
"075-embedding-failure-event-dispatch-on-provider-unavailable",
220+
"076-embedding-event-mutual-exclusion",
221+
"077-embedding-event-call-id-distinct",
222+
"078-embedding-event-input-strings-populated",
223+
"079-embedding-event-request-params-populated",
224+
"080-embedding-event-input-count-and-dimensions-populated",
225+
"081-embedding-event-active-prompt-populated",
226+
"082-otel-embedding-span-attributes",
227+
"083-langfuse-embedding-observation",
228+
"089-embedding-metrics-token-and-duration",
229+
)
230+
},
231+
}
232+
233+
234+
# _UNIT_TESTED_FIXTURES — implemented behavior covered by the dedicated unit
235+
# suite rather than wired into this YAML harness. Value names the proposal +
236+
# the covering file.
237+
_UNIT_TESTED_FIXTURES: dict[str, str] = {
238+
fixture_id: reason
239+
for fixture_ids, reason in (
240+
(
241+
("022-langfuse-basic-trace", "023-langfuse-generation-rendering", "024-langfuse-prompt-linkage"),
242+
"proposal 0031 Langfuse mapping; covered by test_observability_langfuse.py",
243+
),
244+
(
245+
(
246+
"031-langfuse-subgraph-span-hierarchy",
247+
"032-langfuse-fan-out-per-instance-spans",
248+
"033-langfuse-detached-trace-mode",
249+
),
250+
"proposal 0035/0061 Langfuse span hierarchy; covered by test_observability_langfuse.py",
251+
),
252+
(
253+
(
254+
"027-langfuse-caller-supplied-metadata",
255+
"029-caller-metadata-fan-out-per-instance",
256+
"034-caller-metadata-open-span-update-serial",
257+
),
258+
"proposal 0034/0040 caller metadata; covered by test_observability_langfuse.py",
259+
),
260+
(
261+
("030-caller-metadata-parallel-branches-per-branch",),
262+
"proposal 0040 per-branch caller metadata; covered by test_observability_otel.py",
263+
),
264+
(
265+
("035-caller-invocation-id-uuid", "036-caller-invocation-id-non-uuid"),
266+
"proposal 0039 invocation_id derivation; covered by test_observability_langfuse_adapter.py",
267+
),
268+
(
269+
("037-langfuse-trace-input-output",),
270+
"proposal 0043 trace input/output; covered by test_observability_langfuse.py",
271+
),
272+
(
273+
(
274+
"043-get-invocation-metadata-roundtrip",
275+
"044-get-invocation-metadata-fan-out-scoping",
276+
"045-get-invocation-metadata-retry-scoping",
277+
"046-get-invocation-metadata-outside-invocation",
278+
),
279+
"proposal 0048 get_invocation_metadata; covered by test_observability_metadata.py",
280+
),
281+
(
282+
("059-implementation-attribution-langfuse",),
283+
"proposal 0052 implementation attribution; covered by test_observability_langfuse.py",
284+
),
285+
(
286+
(
287+
"060-llm-completion-event-input-messages-populated",
288+
"061-llm-completion-event-output-content-populated",
289+
"062-llm-completion-event-request-params-populated",
290+
"063-llm-completion-event-request-extras-populated",
291+
"064-llm-completion-event-active-prompt-populated",
292+
"066-llm-completion-event-active-prompt-group-populated",
293+
"067-llm-completion-event-call-id-always-present-and-distinct",
294+
"068-llm-completion-event-response-model-distinct-from-request",
295+
),
296+
"proposal 0057 LlmCompletionEvent fields; covered by test_llm_provider.py",
297+
),
298+
(
299+
("065-llm-completion-event-active-prompt-null",),
300+
"proposal 0057 active_prompt null case; covered by test_observability_otel.py",
301+
),
302+
(
303+
(
304+
"069-llm-failure-event-dispatch-on-provider-unavailable",
305+
"070-llm-failure-event-dispatch-on-provider-invalid-request",
306+
"071-llm-failure-event-call-id-distinct-from-completion-event",
307+
"072-llm-failure-event-mutual-exclusion-with-completion-event",
308+
"073-llm-failure-event-error-type-vendor-specific",
309+
),
310+
"proposal 0058 LlmFailedEvent; covered by test_llm_provider.py",
311+
),
312+
)
313+
for fixture_id in fixture_ids
314+
}
315+
316+
317+
# _CONVENTION_ONLY_FIXTURES — proposal 0048 §9 queryable-observer pattern is
318+
# convention-only (no new abstract surface on Observer), satisfied via
319+
# docs/concepts/observability.md, so there is no library API to assert.
320+
_CONVENTION_ONLY_FIXTURES: dict[str, str] = {
321+
fixture_id: (
322+
"proposal 0048 §9 queryable-observer pattern is convention-only "
323+
"(no library surface); satisfied by docs/concepts/observability.md"
324+
)
325+
for fixture_id in (
326+
"047-queryable-observer-pattern",
327+
"048-queryable-observer-async-safety",
328+
"049-queryable-observer-lifecycle-drop",
329+
)
204330
}
205331

206332

@@ -224,6 +350,37 @@ def _load(path: Path) -> dict[str, Any]:
224350
return cast("dict[str, Any]", yaml.safe_load(f))
225351

226352

353+
def test_observability_fixture_coverage_is_complete() -> None:
354+
# Fail-on-unknown guard. Every pinned observability conformance fixture
355+
# MUST be either run (_SUPPORTED_FIXTURES) or explicitly accounted for:
356+
# _DEFERRED_FIXTURES (future capability), _UNIT_TESTED_FIXTURES (covered
357+
# by the unit suite, not this YAML harness), or _CONVENTION_ONLY_FIXTURES
358+
# (doc-satisfied, no library surface). A new spec fixture that is none of
359+
# these fails HERE rather than silently pytest.skip-ping past CI.
360+
all_ids = {p.stem for p in _fixture_paths()}
361+
accounted = (
362+
set(_SUPPORTED_FIXTURES)
363+
| _DEFERRED_FIXTURES.keys()
364+
| _UNIT_TESTED_FIXTURES.keys()
365+
| _CONVENTION_ONLY_FIXTURES.keys()
366+
)
367+
unaccounted = sorted(all_ids - accounted)
368+
assert not unaccounted, (
369+
"unaccounted observability conformance fixtures: wire each into "
370+
"_SUPPORTED_FIXTURES once it runs, or document it in _DEFERRED_FIXTURES "
371+
"(future capability) / _UNIT_TESTED_FIXTURES (covered by the unit suite) "
372+
f"/ _CONVENTION_ONLY_FIXTURES (doc-satisfied): {unaccounted}"
373+
)
374+
# An accounting entry whose fixture no longer exists on disk (renamed at
375+
# a pin bump) should be removed.
376+
stale = sorted(accounted - all_ids)
377+
assert not stale, f"accounting entries with no fixture file (remove): {stale}"
378+
# A fixture cannot be both run and documented-as-not-run.
379+
not_run = _DEFERRED_FIXTURES.keys() | _UNIT_TESTED_FIXTURES.keys() | _CONVENTION_ONLY_FIXTURES.keys()
380+
overlap = sorted(set(_SUPPORTED_FIXTURES) & not_run)
381+
assert not overlap, f"fixtures both run and documented-as-not-run (pick one): {overlap}"
382+
383+
227384
# ---------------------------------------------------------------------------
228385
# Per-fixture dispatcher
229386
# ---------------------------------------------------------------------------
@@ -232,10 +389,18 @@ def _load(path: Path) -> dict[str, Any]:
232389
@pytest.mark.parametrize("fixture_path", _fixture_paths(), ids=_fixture_id)
233390
async def test_observability_fixture(fixture_path: Path) -> None:
234391
fixture_id = fixture_path.stem
235-
if fixture_id in _DEFERRED_FIXTURES:
236-
pytest.skip(f"{fixture_id}: {_DEFERRED_FIXTURES[fixture_id]}")
392+
skip_reason = (
393+
_DEFERRED_FIXTURES.get(fixture_id)
394+
or _UNIT_TESTED_FIXTURES.get(fixture_id)
395+
or _CONVENTION_ONLY_FIXTURES.get(fixture_id)
396+
)
397+
if skip_reason is not None:
398+
pytest.skip(f"{fixture_id}: {skip_reason}")
237399
if fixture_id not in _SUPPORTED_FIXTURES:
238-
pytest.skip(f"{fixture_id}: harness wiring not yet implemented")
400+
# Unaccounted: neither wired nor documented. The coverage guard
401+
# (test_observability_fixture_coverage_is_complete) fails loudly
402+
# listing every such fixture; the individual case skips here.
403+
pytest.skip(f"{fixture_id}: unaccounted -- see the coverage guard")
239404

240405
spec = _load(fixture_path)
241406
if fixture_id == "001-otel-basic-trace":

0 commit comments

Comments
 (0)