Activate fixture 157 against a real Langfuse client - #271
Merged
Conversation
Conformance-adapter 6.4 asks for a provider-faithful double so the isolation obligations can be gated. The two langfuse_client modes need different things, so they get different doubles. mode: supplied hands over an object satisfying openarmature's client protocol, which is the new fake: it records observations and emits them through its bound TracerProvider, so a leak is observable. mode: credentials cannot be faked. There openarmature builds the client itself and wraps it in an adapter that drives the SDK's private surface, and faking that was tried: every internal satisfied uncovered another, and provider observations would not have landed in a recorded side anyway. So it uses a real client with only its egress removed, which leaves the client, the adapter, the binding and the per-credential singleton genuinely real. Because a real client has no recorded side, the fixture's expected observation tree is read off the provider openarmature isolated. That is the fixture's own declared non-vacuity proof, so no-leak cannot be satisfied by a run that emitted nothing. Also adds a guard on the private SDK symbols the shipped adapter depends on. The only prior guard was the live-account test, which CI deselects, so nothing checked them on a normal run despite a declared range of >=4.6,<5. Case and expected directives now have per-runner allowlists. A file-global set let one runner's implementation vouch for a key another silently dropped, and keys nothing reads are recorded by name with a reason rather than sitting in the implemented set. That surfaced a live silent skip: langfuse_observer_config was read nowhere, so fixture 059's two cases both ran at the default.
There was a problem hiding this comment.
Pull request overview
Activates observability conformance fixture 157 by extending the Langfuse conformance harness to exercise provider-isolation behavior against (a) a provider-faithful supplied-client fake and (b) a real Langfuse SDK client with its network egress neutralized, plus adds CI-visible guards for the private Langfuse SDK internals the shipped adapter relies on.
Changes:
- Adds a provider-faithful Langfuse client fake (exports real
langfuse.*span attributes via a boundTracerProvider) and a “real SDK, no egress” harness context for credential-mode cases. - Updates the Langfuse conformance runner to run fixture 157 with stricter directive/expectation allowlists and leak-assertion accounting.
- Bumps
langfuseinuv.lockto 4.7.1 and updates conformance proposal notes to reflect fixture 157 now running.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Bumps Langfuse SDK to 4.7.1. |
| tests/unit/test_langfuse_sdk_internals.py | Adds unit tests that guard the private Langfuse SDK symbols the shipped adapter depends on. |
| tests/unit/test_langfuse_provider_fake.py | Drives the new provider-faithful fake against a real TracerProvider + in-memory exporter. |
| tests/conformance/test_unimplemented_assertion_guard.py | Strengthens the guard to ensure leak-assertion keys are either implemented or explicitly guarded. |
| tests/conformance/test_observability.py | Moves fixture 157 from deferred to supported for observability conformance runs. |
| tests/conformance/test_observability_langfuse.py | Adds fixture-157 dedicated runner, directive allowlists, and observer-config normalization. |
| tests/conformance/harness/langfuse_real_client.py | Introduces a context manager that runs a real Langfuse client with its exporting processor swapped out and cache/env sanitized. |
| tests/conformance/harness/langfuse_provider_fake.py | Introduces the provider-faithful supplied-client fake and span selection helper. |
| conformance.toml | Updates proposal notes to reflect fixture 157 activation and remaining 158 deferrals. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The three identity leak keys were spelled out in three places, so they could drift apart silently. The expected-directive sets now derive from the implemented-leak set instead of restating it, which also makes the reference intra-module and clears a CodeQL unused-global alert without a suppression comment; the variable was never unused, only read from a sibling module. Private OTel globals get the type-ignore pair the rest of the repo uses when touching them, and the SDK version check parses its major.minor with a regex. A PEP 440 two-component pre-release attaches its suffix to the minor, so splitting raised on a version inside the declared range, and a bare ValueError from a guard test reads the same as a genuine range violation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Activates observability fixture 157, which gates the two provider-isolation MUSTs from proposal 0114: that openarmature isolates the Langfuse client it builds itself, and that it never mutates one the caller supplies.
Two modes, two doubles
langfuse_clienthas two modes and they need different things.mode: suppliedhands openarmature an object satisfying its client protocol. That is the new fake: it records observations as the bundled in-memory client does, and also emits each one through its boundTracerProvider, the way a Langfuse v4 client does, so a leak onto a shared provider becomes observable. Its spans carry the SDK's reallangfuse.*attributes rather than a harness-invented summary, because a double that stamps its own verdict and an assertion that reads that verdict together test the double's opinion instead of the property.mode: credentialsis not faked, and this is the substantive design decision in the PR. There openarmature constructs the client itself and wraps it inLangfuseSDKAdapter, which drives the SDK's private surface:start_observation,_otel_tracer,_create_remote_parent_span,_get_otel_trace_id, and the reallangfuse._client.spanclasses for back-dated observations. Faking that was tried first. Each internal satisfied uncovered another, and provider observations would not have landed in a recorded side regardless, because the adapter builds real SDK span objects for them.So that mode uses a real client with only its egress removed. The SDK attaches its own exporting span processor to whatever provider it is given; replacing that one class leaves the client, the adapter path, the provider binding and the per-credential singleton genuinely real, and the fixture's own exporter still sees every span. It is more faithful than the fake would have been, and it makes
preexisting_same_key_clientreal for the follow-up rather than simulated.Non-vacuity
A real client has no recorded side, so the fixture's expected observation tree is read off the provider openarmature isolated. That tree is what the fixture itself labels its non-vacuity proof, and using it means "no observation leaked" cannot be satisfied by a run that emitted nothing. An earlier draft substituted a weaker check of its own, which the node span alone satisfied.
Both MUSTs are mutation tested. Breaking isolation fails the mode-b case; rebinding a supplied client fails the mode-a case; renaming the Generation or suppressing its output despite the payload flag fails the tree assertion.
A guard on the private SDK surface
The shipped adapter reaches for private symbols inside the declared
langfuse>=4.6,<5range. The only guard was the live-account integration test, which CI deselects, so on a normal run nothing checked them.tests/unit/test_langfuse_sdk_internals.pyenumerates them and fails on a rename, which matters because losing one does not raise to the caller: the graph observer isolates observer errors, so an observation simply stops being emitted and a leak assertion reads clean.uv.lockmoves to langfuse 4.7.1, matching the version the compatibility page records as verified.Directive allowlists
Case-level and
expected-level keys now have allowlists, so a key nobody reads fails loudly instead of being ignored while the case still reads like coverage. Two properties are deliberate:Building them surfaced a live silent skip that predates this PR:
langfuse_observer_configwas read nowhere, so fixture 059's two cases both ran at the default and its state-payload-enabled case was not exercising the configuration it names. The spelling is now folded ontolangfuse_observerat load. Note that this makes the directive take effect; whether 059's assertions distinguish its two cases is a separate fixture-design question and is left alone.Testing
2094 passed, 504 skipped. Ruff, pyright and
check_conformance_manifest.pyclean. Conformance and unit suites re-run for order contamination, which is the real risk here: the runner mutates and restores three process-global things per case, and each is snapshotted inside the block that restores it.conformance.tomlis reconciled: three notes still said the §6.4 double was unbuilt and 157 deferred.Follow-ups
Fixture 158 stays deferred on the payload-bearing classification,
expected_construction_error, thelevelkey onexpected.log_records, and thepreexisting_same_key_client/accept_shared_providersub-directives. Those sub-directives and thelog_recordskey are recorded as named deferrals rather than quietly allowlisted, so activating 158 has to address them.The
langfuse>=4.6,<5floor is worth revisiting separately: the code cites v4.7-specific behaviour in several places, and the range currently resolves to 4.14.x.