Repository navigation
test(sys-modules): scope six leaking stub owners the baseline never saw (#13450) - #13458
Merged
Merged
Conversation
…eed them (#13450) Each of the four installed a sys.modules stub that outlived its own directory, so the leak guard reported them as unlisted offenders and failed any session whose path set let them escape. - llm_shared/structured_ops_test.py: utils / utils.semantic_chunker now go in through monkeypatch.setitem and come back out at teardown. - tests/security/test_a2a_trust_gate.py: the a2a.task_manager stand-in is built once and registered per test; every api.a2a import is inside a test body, so the import-time install was never needed. - tests/agents/test_learned_prompt_template_wiring.py: the hollow agents packages wrap the module-level import in try/finally and only the keys nothing else provided are removed again. - tests/unit/chat_workflow/test_inline_judge.py: graph.py is exec'd inside every test by _load_graph, so the langchain/langgraph stand-ins are registered per test instead of at module import. tests/test_claude_adapter_wiring.py's anthropic stand-in uses setdefault plus an identity check on the way out, so a real, already imported SDK is never displaced and never deleted. Also renames the second TestRouteAfterJudge class in test_inline_judge.py: two classes of the same name meant pytest only ever collected the later one and the four cases above it were silently dropped (13 collected -> 17). The baseline is untouched: it only shrinks.
…gy_routing_wiring (#13450) Same two keys and the same shape as test_learned_prompt_template_wiring: a hollow agents.agent_orchestration installed at module import and never removed. It was invisible while that file installed them first — setdefault is first-writer-wins, so only one owner is ever blamed — and surfaced the moment that file stopped leaking.
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
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.
Thinking Path
The guard became a ratchet in #13442: an owner that leaks and is not on
repo_tests/sys_modules_leak_baseline.txtfails the run. Five owners were absent becausethe baseline was generated from only the two invocation shapes CI uses, so every session
with a different path set reported pre-existing debt as a regression — the failure mode
#13398 exists to prevent.
The baseline only shrinks, so none of these could be listed. Each was fixed by binding its
stub to the window that actually needs it, per #13359's pattern
(
autobot-backend/llc/tests/conftest.py): build the stub objects once somock.patch'sgetattrresolution keeps hitting the same object, install only the keys nothing else hasalready provided, and remove exactly those again from a
try/finallyor throughmonkeypatch.Where the stub was only needed while tests run,
monkeypatch.setitemis the whole fix —its undo runs inside
pytest_runtest_protocol, i.e. before the guard attributes thenode's delta, so the session sees no mutation at all. Only the two files with a
module-level import that genuinely needs the stub keep an install, and they wrap exactly
that import.
A sixth owner appeared the moment the fifth stopped leaking:
setdefaultisfirst-writer-wins, so
test_topology_routing_wiring.pyhad been invisible behindtest_learned_prompt_template_wiring.pyfor the same two keys. It is fixed the same way,which is why this PR covers six files rather than five.
The sixth of the five listed owners —
api/multimodal_response_test.py's fourtransformers.*keys — is not fixed here. It is not a stub at all, and the analysisand recommendation are below, left for a decision.
What Changed
Six test modules; no production code, no guard change, no baseline change.
autobot-backend/llm_shared/structured_ops_test.pyutils,utils.semantic_chunkermonkeypatch.setitem+monkeypatch.setattrinside the one test that needs themautobot-backend/tests/security/test_a2a_trust_gate.pya2a.task_managerapi.a2aimport is already inside a test body, so the import-time install was never neededautobot-backend/tests/unit/chat_workflow/test_inline_judge.pylangchain_core.*/langgraph.*graph.pyisexec'd inside every test by_load_graph, so one test is the whole windowautobot-backend/tests/agents/test_learned_prompt_template_wiring.pyagents.agent_orchestration,…rl_routertry/finallyaround the module-level importautobot-backend/tests/agents/test_topology_routing_wiring.pytry/finallyautobot-backend/tests/test_claude_adapter_wiring.pyanthropicsetdefault+ delete-only-if-still-ours around the provider importTwo details that are load-bearing rather than tidiness:
types.ModuleTypemutatesnothing, so it is safe outside the window; a fresh object per test would move the
attributes
mock.patchand the code under test resolve against (test-infra: llc/tests conftest leaks a sys.modules['agents'] stub — third conftest leak in two days #13337's trap)._INSTALLED/ thesetdefaultidentity check mean a real, already-imported package is never displaced and never
deleted — including the real
anthropic, which CI installs.Pre-existing test rot fixed in passing (
test_inline_judge.py): two classes wereboth named
TestRouteAfterJudge, so the later one rebound the module attribute and thefour cases in the first were silently dropped from the suite. The second is renamed
TestRouteAfterJudgeRegenerationCap; collection goes 13 → 17 and all four recoveredcases pass.
Verification
Every "before" was captured by restoring the six files to
origin/Dev_new_guiwithgit restore --source=67d47406band re-running the identical command.Per owner — a session that actually exercises the escape:
structured_ops_test.pyLEAK: … leaves 1 sys.modules key(s) … keys: utils.semantic_chunkertest_a2a_trust_gate.pyLEAK: … keys: a2a.task_manager(escapes totests/services)test_learned_prompt_template_wiring.pyLEAK: … keys: agents.agent_orchestration, agents.agent_orchestration.rl_routertest_claude_adapter_wiring.pyLEAK: … keys: anthropic(escapes toautobot_shared)test_inline_judge.pyLEAK: … leaves 9 sys.modules key(s) … langchain_core, langgraph, …test_topology_routing_wiring.pyLEAK: … keys: agents.agent_orchestration, agents.agent_orchestration.rl_routerThe session from the issue, verbatim,
-n auto --dist loadscope:before (files restored to base)
after
No
LEAK:line, noFIXED:line — the baseline needs no edit.Both CI shapes, collect-only:
Failure-set diff, before vs after, same command:
+11554 −11551passed (the fourrecovered
TestRouteAfterJudgecases, minus one) and exactly one failure that is not inthe before set:
That one is pre-existing and independent of this PR — reproduced deterministically on
unmodified files, in a session that contains none of the six:
Two baselined
sys.modulesjugglers restore overlapping keys at module teardown; whenthe deal puts them on one worker the
MagicMockstub is back beforetest_recovery_plan_serialization's function-local re-import runs, andto_dict()dieswith
TypeError: asdict() should be called on dataclass instances. Filed as #13457.Adding four tests here is enough to shift the deal, which is all this PR did to it.
Lint on all six:
black --checkclean,flake8clean,ruff checkclean. The oneflake8finding intest_topology_routing_wiring.py(F401 AgentTopology) is byte-for-bytepresent on
origin/Dev_new_guiand untouched here.Model Used
Sonnet (claude-sonnet-4-6)
Left for a decision:
api/multimodal_response_test.pyand the fourtransformers.*keysNot fixed here, deliberately. The full analysis is in the issue thread; the short version:
Where the entries come from.
multimodal_response_test.pycontains nosys.modulesstatement, and neither does anything it reaches.
grepacross the whole repo finds zerowrites to any
transformers*key. The chain ismultimodal_response_test.py:19 → api/multimodal.py:17 → ai_hardware_accelerator.py:44-50:The four entries are produced entirely inside the real library by its own lazy-module
machinery; the guard reports them as
replacedbecause the keys already existed when thefrom transformers import …above materialised them, and it attributes the delta to themodule whose import produced it.
It is a real dependency, installed in CI.
requirements-ci/ai-ml.txt:4pinstransformers==5.14.1. It is not in the local dev venv, which is why_shadows_real_codefilters it out locally and why this owner cannot be reproduced orverified outside CI.
Nothing "relies on the entries" that AutoBot could stop relying on. They are the
library's own module registry, not a stand-in for anything. The only AutoBot state
involved is
MULTIMODAL_MODELS_AVAILABLE, read atai_hardware_accelerator.py:672.Recommendation — treat it as a guard false positive, not a leak. Nothing is being
stubbed here, so there is no
try/finallyto add: the only production change available ismaking the import lazy, which does not remove the delta, it relocates the blame to
whichever test first touches the multi-modal path, and it turns a module-level constant
read elsewhere into a call. That is churn with real blast radius and no fix in it.
The narrow alternative is a one-line refinement in
_classify: areplacedentry whosenew object is a real, spec-carrying module the import system loaded is not a stub. It
cannot mask the three leaks the guard exists for — #13320 replaces
sqlalchemywith aMagicMock, #13324 replacesautobot_sharedwith aMagicMock, #13337 replacesagentswith a
types.ModuleType, and all three read as__spec__ is None(Mock.__getattr__raises on dunder names, so
getattr(mock, "__spec__", None)isNone).It does have a cost, which is why it is a decision and not a unilateral change: at least
one baselined owner replaces with a real-loaded module —
llm_shared/npu_pipeline_routing_test.pybuilds its replacement withmodule_from_spec(spec_from_file_location(...)), which carries a__spec__. Refining_classifywould silence it, and the shrink-only rule would then fail every run withFIXED: … remove it from …until its line is deleted. So the change is "refine_classifyand delete the baseline lines it legitimately retires", which needs the owner's call on
how many lines that is.
Refs #13450
Refs #13361