@@ -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 )
233390async 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