Repository navigation
test-infra(leak-guard): base branch red — 4 transformers keys misattributed to a test that references neither transformers nor sys.modules #13599
Description
Symptom
The python-suite job fails on Dev_new_gui — all 12 shards, current base head. The failure is not a test failure. Both pytest invocations in the shard are green:
======== 2070 passed, 2125 skipped, 2692 warnings in 102.50s (0:01:42) =========
====================== 497 passed, 38 warnings in 28.21s =======================
The non-zero exit comes from the leak guard:
============================ sys.modules leak guard ============================
LEAK: autobot-backend/api/multimodal_response_test.py leaves 4 sys.modules key(s)
installed outside autobot-backend/api/ — first seen at ...
keys: transformers.tokenization_utils_fast, transformers.tokenization_utils,
transformers.image_processing_utils_fast, transformers...
1 file(s) above are NOT on repo_tests/sys_modules_leak_baseline.txt, so this run fails.
The accused file cannot be the leaker
autobot-backend/api/multimodal_response_test.py contains no reference to transformers and no reference to sys.modules. Its only test double is unittest.mock.MagicMock, used as a plain object:
grep -n "transformers\|sys.modules" autobot-backend/api/multimodal_response_test.py
# -> no matches for either
Run on its own it is clean and the guard says nothing:
python3 -m pytest autobot-backend/api/multimodal_response_test.py -q -p no:randomly
======================== 7 passed, 36 warnings in 1.60s ========================
So the attribution is wrong: the keys are installed by some other node, and the file that happens to own the snapshot boundary when the delta is first observed gets named.
Why this is the guard's bug, not the file's
_LeakGuard.snapshot() attributes the whole delta since the last snapshot to the node currently being visited. Any key installed while a sibling ran, but first observed at this node's boundary, lands on this owner. The module docstring already documents one instance of this failure mode — an ancestor Dir collector "stole the first seen at slot from the sibling that actually inherited the stub, discarding the actionable half of the report" — and that fix exempted ancestor directories only. This is the same defect between siblings, which that fix does not cover.
It is also composition-dependent: pytest-split re-composes shards whenever durations change, so which node owns the boundary moves between runs. That makes the gate intermittent — it will appear to "fix itself" and come back.
The wrong fix
Adding multimodal_response_test.py to repo_tests/sys_modules_leak_baseline.txt. The baseline is shrink-only and exists to record real owners; baselining a misattributed file both launders a real leak somewhere else and permanently blames an innocent module. The named file has nothing to fix.
What is actually needed
- Attribute a mutation to the node that was executing when the key appeared, not to the node that owns the next snapshot boundary — or, where that is not recoverable, report the candidate set instead of picking one and asserting it.
- Identify the real owner of the four
transformers.*keys.autobot-backend/conftest.pyis already baselined at 47 keys and stubs package parents (conftest.py:63,102,106), which makes it the first place to look. - Until attribution is trustworthy, the guard should not be able to red the base branch on a file that has no way to fix itself.
Blast radius
python-suite is not a required check (#13162), so this does not block merges — but it does mean the base-branch signal restored by #13432 reads red for a reason unrelated to any test, which is exactly the confusion #13432 set out to remove.
Guard introduced in #13398 / #13442. Related: #13361 (conftest leaks 62 keys), #13450 (_classify refinement), #13554 (orphan checker is regex-based).
Activity
mrveiss commentedon Aug 4, 2026
Narrowing the cause — the four keys are almost certainly real transformers submodules, not stubs
Investigated further. This changes what the fix should be, so recording it before anyone starts on the attribution rework.
Nothing in the repo creates those keys. A repo-wide search for the flagged names returns nothing:
They are also absent from both stub lists in
autobot-backend/conftest.py(_SIMPLE_STUBSat :137 — prometheus_client, xxhash, torch, torch.nn, torch.cuda, asyncpg, psycopg2, alembic;_PKG_STUBSat :1132 — sqlalchemy.* and alembic.*). No conftest asks fortransformers.But transformers is installed in CI —
requirements-ci/ai-ml.txt:4pinstransformers==5.14.1. So these are that package's own submodules, appearing through an ordinary lazy import.Why a real module still trips the guard
_is_syntheticis a single attribute probe:transformers builds its public surface out of
_LazyModuleobjects rather than importing submodules eagerly. A lazily-materialised submodule registered insys.moduleswithout a populated__spec__reads as__spec__ is None— which is exactly the stub signature. From there_shadows_real_code("transformers.tokenization_utils")is true (the package is installed), so the entry is reported as a leak.That makes the docstring's premise wrong for this family:
__spec__ is Nonedoes not mean "hand-built", it means "not created by the normal import machinery" — and a third-party lazy-import system is the other thing in that set. The_shadows_real_codeguard was designed for the opposite case (cffi/Cython pseudo-modules that shadow nothing); here the name does resolve to something installed, so it passes.Status: unverified.
transformersis not installed in this environment and I am not adding it — optional test deps areimportorskip-gated by policy, not ad-hoc installs. The one-line check for anyone who has it:If
__spec__isNone, this is confirmed and the misattribution in the original report is a symptom, not the cause.What this changes about the fix
The attribution rework described above is still worth doing — blaming a file that contains neither
transformersnorsys.modulesis bad either way. But it would not fix this failure: correct attribution would simply blame whichever module first touched transformers, and that module is not leaking anything. The guard would still red the base branch.The narrower fix is in
_classify/_is_synthetic: treat a module as synthetic only when it is also not a real importable package under that exact name, or exempt entries whose object is aModuleTypesubclass originating from an installed distribution. #13450 is already open on_classifyrefinement and needs a decision — this is concrete input for it.Cross-referencing #13450 and #13361.
mrveiss commentedon Aug 4, 2026
Persisting across every base run — and not caused by the day's merges
Re-checked on a later base head (
203c69de6, run30951203696) after four more PRs landed. Identical failure, same file, same four keys:Three consecutive
AutoBot CI/CD Pipelineruns onDev_new_guihave now failed this way (cc4c9f585,203c69de6, and the current head still running). Zero test failures in every one — the non-zero exit is the guard alone. So the base-branch signal that #13432 restored reads red for a reason unrelated to any test, which is precisely the confusion #13432 existed to remove.Why I have not just fixed it
The obvious patch — stop treating a
__spec__-less entry as a stub when the name resolves to an installed package — would break the guard's core purpose._shadows_real_codedeliberately requires exactly that condition: a stub is dangerous because it shadows something real. The three historical leaks it was built to catch (sqlalchemy,autobot_shared,agents) all shadow installed packages, and every one of them would stop being detected.So the correct fix has to separate two things that currently look identical:
MagicMockover an installed package and leaves it behind — must stay caught__spec__-less submodule of itself — must be ignoredA plausible discriminator is ownership: a real package's lazy submodule is an object whose parent package is the genuine distribution, whereas a leaked stub is not. That is a design decision with real consequences either way, which is why this belongs on #13450 rather than being patched unilaterally at 1am.
What is needed from you
One decision on #13450, then the fix is small. Until then the base branch stays red on every merge, and
python-suiteremains unusable as a signal — which also blocks measuring #13162.The hypothesis in my previous comment (transformers
_LazyModulesubmodules carrying__spec__ is None) is still unverified —transformersis not installed here and optional test deps areimportorskip-gated by policy rather than installed ad hoc. The one-line check is in that comment.mrveiss commentedon Aug 4, 2026
Hypothesis CONFIRMED, and the cause is worse than "lazy import"
Found
transformers==5.14.1already installed in a local CI-shaped venv, so this is now measured rather than guessed — no ad-hoc install involved.So
_is_synthetic(__spec__ is None) answers True for all three, and_shadows_real_codepasses because the names are importable — hence the leak report.But they are not lazy-loaded real modules. The files do not exist at all:
transformers 5.x removed these modules and installs synthetic backward-compatibility shims into
sys.modulesunder the old names. They are, by every property the guard can inspect, exactly what a hand-built stub is — because that is essentially what they are, just authored by the library instead of by a test.No attribute can separate them — I tried
__file__,__loader__,__spec__and object type all fail to discriminate: the shim is byte-for-byte indistinguishable fromtypes.ModuleType('x'). Any attribute-based patch either keeps the false positive or stops catching real stubs.A discriminator that does work: is the parent genuine?
A library's own shim always sits under a real, spec-carrying parent package. A leaked stub replaces the package itself.
Measured against both populations:
transformers.tokenization_utilstransformers.tokenization_utils_fasttransformers.image_processing_utils_fastsqlalchemysqlalchemy.ormagentsagents.coreAll three historical leaks stay detected, because a test stubbing a package replaces the top-level entry, which drops the parent's
__spec__.The blind spot, stated plainly
This trades away one case — a test that stubs only a submodule while leaving the real parent intact:
That is a real leak the guard would stop catching. Whether it is an acceptable trade depends on whether anything in this repo stubs a child without its parent; the two stub lists in
autobot-backend/conftest.pydo not (_PKG_STUBSalways installs the parent alongsidesqlalchemy.ext,sqlalchemy.orm, …), so on current evidence the exchange costs nothing real and buys back a green base branch.That is the decision for #13450 — I am not making it unilaterally. But it is now a concrete either/or with measurements on both sides rather than an open question.
fix(test-infra): stop the leak guard reporting a real package's own s…
github-actions commentedon Aug 5, 2026
PR #13620 (merged to
Dev_new_gui) references this issue with a close keyword.If this issue is fully resolved, close it manually. If work remains, no action is needed.
mrveiss commentedon Aug 5, 2026
Fixed — landed as
0bcef05e6(PR #13620).The four
transformers.*keys are no longer reported. Root cause, measured:transformers==5.14.1removedtokenization_utils,tokenization_utils_fastandimage_processing_utils_fastand installs spec-less compatibility objects under the old names — the files are not on disk. So_is_syntheticwas correct about them; they are genuinely synthetic, just authored by the library rather than a test.No attribute of the entry could separate the two cases (
__spec__,__file__,__loader__and object type are identical totypes.ModuleType("x")), which is why the obvious__file__patch would not have worked. Ownership of the parent does separate them, and the owner chose that fix from four options.Verified against both populations with real transformers installed:
The misattribution this issue reported is a symptom of the same cause and goes with it. The accepted trade — a test stubbing only a submodule while leaving the real parent intact is no longer caught — is recorded in the code and pinned by a renamed test, with a new sibling test asserting that a stubbed parent still drags its children into the report.
mrveiss commentedon Aug 5, 2026
Correction — my fix did not resolve this. Reopening.
I closed this on PR #13620 saying the four keys would no longer be reported. They still are. Base run
31001085706ond94f032f9, which contains0bcef05e6:What I got wrong
transformersitself is one of the four keys. I read the earlier report as three submodules and did not notice the top-level entry in the list. That single fact invalidates the diagnosis:_parent_is_genuinecorrectly returnsFalseand the exemption never applies — by design.requirements-ci.txt:14pulls inrequirements-ci/ai-ml.txt, which pinstransformers==5.14.1, so transformers is genuinely installed in this job. A real installed package does not appear as a suspicious top-level mutation.So something replaces or synthesises
transformersitself during the shard, and the three submodules follow it. That is a real leak of exactly the kind this guard exists to catch — not a false positive.My verification was the flaw: I tested
_parent_is_genuinein isolation against a cleanly importedtransformersand confirmed the predicate did what I intended. I never reproduced the CI classification path, where the parent is tracked and the entries reach_classifydifferently. Testing the predicate is not testing the fix.Is #13620 still correct?
The change itself stands on its own terms — a new spec-less child of a genuinely untouched package should not be reported, both new tests pass, and no existing detection was lost. It simply does not address this failure. I am not reverting it; I am reopening this issue because the base branch is still red for the original reason.
Next
Identify what installs a
transformersentry during shard 1 — the two_make_transformers_mock()helpers inllm_shared/optimization/pass mocks as function arguments and never touchsys.modules, so it is not those. Once found, the fix is the ordinary one this guard asks for: install and remove the stub in the sametry/finally, scoped to the nodes that need it.mrveiss commentedon Aug 5, 2026
Root cause — reproduced locally in 2.4s, and it is not a repo leak
Inspecting the entries after that test module imports:
The parent is real — a spec-carrying
_LazyModule, and importing it registers 371transformers.*entries. Only four are reported, which is the tell.Ruled out by measurement, not assumption:
autobot-backend/conftest.pydoes not stubtransformers— imported it directly and the key is absent afterwards._make_transformers_mock()helpers inllm_shared/optimization/pass mocks as function arguments; neither touchessys.modules.requirements-ci.txt:14pullsai-ml.txt, sotransformers==5.14.1is genuinely installed in this job.So the four break down as:
transformerssys.modulesentry to a_LazyModuleduring init, which is a different object than the one the import machinery first installedThat is why #13620 did not help: my exemption requires an untracked parent, and the self-replacement puts the parent in
_trackedfirst.What the fix has to be
Exempt a replacement whose new object is a genuine module re-registering itself — a real
ModuleTypecarrying a__spec__whose__name__equals the key. A library doing its own lazy-init rebind satisfies that; a test swapping in aMagicMockdoes not, because the mock has no valid__spec__. Checked against the three historical leaks:autobot_shared→ bareMagicMock,sqlalchemy→MagicMock,agents→types.ModuleTypewith no spec — all still caught.With the parent no longer tracked, the child exemption from #13620 then applies to the three shims on its own.
I will implement and verify against the local reproduction above rather than against the predicate in isolation — that isolation test is exactly what let the last attempt through.
mrveiss commentedon Aug 5, 2026
Progress, and a regression in my own fix — not pushed
The original failure is fixed locally. The 2.4s reproduction is clean:
Two changes were needed, and the second is the one I had missed twice:
_is_self_rebind— a package re-registering itself under its own name (realModuleType, populated__spec__,__name__ == key) is not a swap. This clears thetransformersparent.transformers.*shim that is mid-import, before the parent has bound its_LazyModule. So the parent-genuine exemption could not apply at classification time, and a tracked mutation was never revisited. That is why fixing_classifyalone moved the count 4 → 3 and no further.The regression: re-checking
_is_synthetic(module) and parent_is_genuine(key)at report time also silences leaks that were previously reported. Measured on the pair from #13457:Those are baselined owners, so they were not failing — but they were being reported, and the shrink-only baseline depends on that reporting to know when an owner is fixed. Silencing them would eventually fail the removal check and, worse, hide the real leaks the guard exists for.
So the report-time re-check is too broad as written. The next step is to establish exactly which keys those two owners leak and why they satisfy
parent_is_genuine, then narrow the re-check — most likely to the self-rebind case plus a check tied to the specific parent that rebound itself, rather than any genuine parent.Nothing is pushed. The branch
issue-13599holds the work in progress;Dev_new_guiis unchanged and still red for the original reason.fix(test-infra): scope the leak-guard exemptions to third-party packa…
github-actions commentedon Aug 5, 2026
PR #13633 (merged to
Dev_new_gui) references this issue with a close keyword.If this issue is fully resolved, close it manually. If work remains, no action is needed.
test-infra: drop the slm api conftest from the leak baseline, it no l…
mrveiss commentedon Aug 6, 2026
Fixed and verified — closing with evidence
Fixed across PR #13620, PR #13633 and PR #13636, all squash-merged to
Dev_new_gui. Closed manually becauseCloses #Ndoes not fire on this branch.The reproduction this issue specified now passes on clean base (
6fa09d97f), in a worktree with nothing of mine applied:No guard report, no misattribution. And the base branch's own CI confirms it — the latest run's
python-suite shard 3/12log contains zero mentions oftransformers, where this issue's four keys used to appear.What the fix turned out to be
The diagnosis took three attempts, and the first two were wrong in ways worth recording:
transformersitself was one of the four tracked keys, so the parent was never "genuine" by that test.transformers5.14.1 replaces its ownsys.modulesentry with a_LazyModuleduring import and registers spec-less compatibility shims for submodules 5.x removed. No attribute distinguishes those from a test stub —__spec__,__file__,__loader__and object type all fail identically. Only "loaded from outside this repository" separates them, which is the correct rule anyway: a guard on this repo's stubs has no business policing a third-party package's own bookkeeping._is_self_rebind()additionally requires the previous object to have been a real module, so repo code re-importing a real module over a real module is not mistaken for a library self-rebind.#13636 then removed
autobot-slm-backend/api/conftest.pyfrom the baseline, which had stopped leaking — the baseline shrinks, never grows.Not closed by this
The base is still red on
python-suite shard 6/12, but for a different cause: three files underautobot-backend/services/knowledge/leakutils.chromadb_client/utils.async_chromadb_client. That is #13651, not this issue — the keys, the owners and the mechanism all differ. #13450 remains open for the five owners the two CI path shapes hide.