Skip to content

C-104: one honest failure instead of twenty-five misleading ones - #282

Merged
Polichinel merged 2 commits into
developmentfrom
fix/c104-venv-drift-is-detectable
Aug 17, 2026
Merged

Polichinel merged 2 commits into
developmentfrom
fix/c104-venv-drift-is-detectable

Conversation

@Polichinel

Copy link
Copy Markdown
Collaborator

The problem

On 2026-08-16 a pytest run in this repository reported 25 failures across five modules, and none was a defect. The virtualenv held views-pipeline-core 2.3.0 and pyarrow 23.0.1 while poetry.lock pinned 3.0.1 and 16.1.0.

Two majors of drift produce:

  • ModuleNotFoundError: views_pipeline_core.modules.dataloaders.datafactory_contract at the import of both managers — which takes out every test that constructs or inspects one, i.e. the only coverage the two largest modules in the package (387 lines each) have;
  • five byte-parity failures against the ADR-013 §10 golden fixture, because parquet bytes are not stable across pyarrow majors (C-72).

From the failure output, neither is distinguishable from a real regression. "You broke the wire contract" is the available reading, and it is the one that costs an afternoon.

What this adds

tests/test_locked_environment.py — two checks:

  1. installed versions of the runtime dependencies declared in pyproject.toml match poetry.lock;
  2. everything declared in pyproject.toml is actually in the lock — the other direction, which would otherwise be reported as a virtualenv problem when it is a stale lock.

Declared, not hardcoded. A literal list goes stale the first time a dependency is added, which is exactly the failure mode tests/conftest.py::PARTNER_PACKAGES exists to prevent one level up.

The failure a developer actually sees:

Failed: this virtualenv is not the one poetry.lock describes:
    views-pipeline-core      installed=2.3.0        locked=3.0.1
    pyarrow                  installed=23.0.1       locked=16.1.0

Run `poetry install`.

Any OTHER failures in this run are most likely consequences of the drift above, not
defects. Two majors of views-pipeline-core remove `modules.dataloaders.datafactory_
contract`, which both managers import at module scope — so every test that touches a
manager dies at collection. A pyarrow major changes emitted parquet bytes, which fails
the ADR-013 §10 byte-parity fixture (C-72). Fix the environment before reading them as
bugs.

If the drift is deliberate — trialling an upgrade — this test is telling you the truth
and the rest of the suite is not testing the locked contract.

What it deliberately does not do

It does not fix the drift and it does not skip. The 25 failures stay until someone runs poetry install. What changes is that a contributor can tell in one line which kind of problem they have — the failures were never wrong, they were unreadable, and that is the whole of C-104's cost.

Dev-group tools are out of scope on purpose: ruff's reported version varies with how it was installed, and the thing that actually broke CI on 2026-08-03 was its rule set, which pyproject.toml already pins explicitly.

Proof

A natural experiment rather than a mutation, and a better one:

environment expected
this developer venv (drifted) fails, with the table above — confirmed
CI (poetry install from the lock) passes — this PR's own run is the check

If CI goes green on this PR, that is simultaneously the negative case for the new test and the standing evidence that the 25 local failures were only ever environment drift.

ruff check . clean. C-104 updated with a Partial mitigation note.

Polichinel and others added 2 commits August 17, 2026 11:44
… (C-104)

On 2026-08-16 a pytest run here reported 25 failures across five modules and none
was a defect. The virtualenv held views-pipeline-core 2.3.0 and pyarrow 23.0.1
while poetry.lock pinned 3.0.1 and 16.1.0. Two majors of drift produce:

  - ModuleNotFoundError on views_pipeline_core.modules.dataloaders.
    datafactory_contract, imported at module scope by BOTH managers, which takes
    out every test that constructs or inspects one — the only coverage the two
    largest modules in the package have;
  - five byte-parity failures against the ADR-013 §10 fixture, because parquet
    bytes are not stable across pyarrow majors (C-72).

From the failure output neither is distinguishable from a real regression, and
"you broke the wire contract" is the reading that costs an afternoon.

tests/test_locked_environment.py compares the installed versions of the runtime
dependencies DECLARED in pyproject.toml against poetry.lock. Declared, not
hardcoded: a literal list goes stale the first time a dependency is added, which
is the failure conftest.PARTNER_PACKAGES exists to prevent one level up.

The failure names each drifted package, both versions, the command, and says
plainly that the other failures are consequences rather than defects — and that
if the drift is deliberate, the rest of the suite is not testing the locked
contract. A second check catches the other direction: declared in pyproject but
absent from the lock, which would otherwise read as a virtualenv problem when it
is a stale lock.

It fixes nothing and skips nothing. The 25 stay until `poetry install` runs. What
changes is that a contributor can tell in one line which kind of problem they
have — the failures were never wrong, they were unreadable.

Dev-group tools are out of scope on purpose: ruff's reported version varies with
how it was installed, and what actually broke CI on 2026-08-03 was its rule set,
already pinned explicitly in pyproject.toml.

Natural experiment rather than a mutation: this FAILS here, against the real
drift, and must PASS in CI, which installs from the lock.

ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eight findings. The first is the one that matters: the guard whose entire purpose
is a readable diagnosis stated something measurably false.

1. "every test that touches a manager dies at collection" — it does not. The
   manager tests import lazily (tests/test_framework_contract.py:50), so the
   drift produces ordinary failures and the session runs to completion. Measured
   in the drifted venv: 26 failed, 441 passed, 0 errors. The claim was also
   self-defeating — pytest interrupts on collection errors, so had it been true
   this diagnostic would never have run in the scenario it was written for.
   Corrected in the message and the module docstring.

2. The consequences paragraph was hardcoded to the two packages that drifted on
   2026-08-16 but fired verbatim for any drift: a views-frames bump would have
   been explained in terms of pipeline-core and parquet bytes. Now conditional on
   which names are actually in the drift set — a diagnosis naming the wrong
   packages is the failure this file exists to remove.

3. No PEP 503 normalization between pyproject keys and lock names. `PyYAML` or
   `views_frames` are legal declarations that would never match a lock entry, and
   the failure would have read "absent from poetry.lock, run `poetry lock`" —
   which fixes nothing, because the lock is fine. Both sides normalized.

4. Dependencies gated by `optional`, `python` or `markers` were treated as
   must-be-installed. They are legitimately absent from a given environment, and
   the message would have said "NOT INSTALLED — run `poetry install`", advice
   that cannot work. It would also have gone red in CI on the 3.11 runner for a
   python-gated dep. Skipped. Verified that `extras`-carrying deps are still
   KEPT, since that is our real views-pipeline-core declaration.

5. The claimed ordering guarantee did not hold: running the lock check first does
   not stop the version check reporting the same omission as `locked=None`. The
   version check now skips names absent from the lock and defers.

6. `installed=None, expected=None` was a silent pass. Same fix as 5 — unlocked
   names are the other test's business, and it says so better.

7. `project["tool"]["poetry"]["dependencies"]` raised a bare KeyError on a PEP 621
   migration — the unreadable failure this module was written against. Now fails
   with the form it expects and an instruction not to delete it, matching
   tests/test_release_version.py's precedent.

8. Register `Last Updated` was still 2026-08-16, stale since C-103's additions.

Verified: normalization on PyYAML/views_frames/Foo.Bar_baz; the conditional-skip
rule against optional/python/markers/extras shapes; the message now naming only
the packages that drifted. ruff clean; register and doc-accuracy guards green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Polichinel

Copy link
Copy Markdown
Collaborator Author

Review round — /review-diff + /code-review high

/review-diff — 1 note, not acted on

tests/test_release_version.py:30 parses pyproject.toml with a regex where this file uses tomllib. The regex is the more fragile of the two and its own message admits it ("no longer declares a version in the form this guard reads"), but converting it is scope creep into a file this PR does not otherwise touch. tests/seam_registry.py already uses tomllib, so the new file follows the newer pattern and the regex is the outlier. Noted, deliberately left.

Also verified rather than defended: poetry.lock has 118 packages, no duplicate names, so building a {name: version} dict cannot silently take the last of two entries. No defensive code added for a case the format does not produce.

/code-review high — 8 findings, all fixed

The first is the one that matters, because it was in the diagnosis itself. The message said "every test that touches a manager dies at collection". It does not — the manager tests import lazily (test_framework_contract.py:50), so the drift produces ordinary failures and the session completes. Measured in the drifted venv: 26 failed, 441 passed, 0 errors. The claim was also self-defeating: pytest interrupts the session on collection errors, so had it been true, this diagnostic would never have run in the scenario it was written for.

The rest:

# finding fix
2 the consequences paragraph was hardcoded to pipeline-core and pyarrow but fired for any drift — a views-frames bump would have been explained in terms of parquet bytes conditional on the actual drift set
3 no PEP 503 normalization: PyYAML or views_frames would never match a lock entry, failing with "run poetry lock" when the lock was fine both sides normalized
4 optional / python / markers deps treated as must-be-installed — would have gone red in CI on the 3.11 runner for a python-gated dep skipped (verified extras-carrying deps are still kept, since that is our real views-pipeline-core declaration)
5 the claimed ordering guarantee did not hold — a stale lock was reported twice, once misleadingly as locked=None the version check skips unlocked names and defers
6 installed=None, expected=None was a silent pass same fix as 5
7 bare KeyError on a PEP 621 migration — the unreadable failure this module was written against fails with the form it expects, per test_release_version.py's precedent
8 register Last Updated stale since C-103 bumped

Findings 2 and 3 are worth dwelling on: a guard that names the wrong packages, or that says "run poetry lock" when the lock is correct, is the same defect this file exists to remove, reintroduced one layer up.

Verification

environment result
CI (poetry install from the lock) 465 passed, 5 skipped, 37 xfailed, 0 failed
this developer venv (drifted) fails with the table, naming only the two packages that actually drifted

That pair is the whole proof: the test passes where the lock is honoured and fails where it isn't — and it is simultaneously the standing evidence that the local failures were only ever environment drift.

Also verified directly: normalization on PyYAML / views_frames / Foo.Bar_baz, and the conditional-skip rule against optional / python / markers / extras shapes.

@Polichinel
Polichinel merged commit b07d953 into development Aug 17, 2026
4 checks passed
@Polichinel
Polichinel deleted the fix/c104-venv-drift-is-detectable branch August 17, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant