Repository navigation
fix(hooks): run autobot-slm-backend as its own pytest invocation (#16069) - #16074
Merged
Merged
Conversation
) pytest.ini says this in as many words: #13084: autobot-backend/ and autobot-slm-backend/ each define top-level packages with identical names (api, services, user_management, middleware, migrations). Running ALL of testpaths in ONE pytest invocation makes whichever backend collects first bind those dotted names in sys.modules for the rest of the session, breaking the other backend's own tests. A full local/CI run MUST use two separate invocations. CI obeys it. The pre-push hook did not. It gathered every changed test into one $all_py_tests and ran a single pytest over it, so a changeset spanning both trees was verified in a shape CI never runs and pytest.ini forbids. The failure that surfaced it looked like somebody else's bug. slm-backend's conftest installs its stubs at import time, so in a combined session sqlalchemy, sqlalchemy.orm, sqlalchemy.ext.asyncio, models and models.database stay MagicMocks while repo_tests collects afterwards. The sys.modules leak guard reported 54 leaked keys and blocked the push -- correctly, and for a condition the changeset had not introduced: the conftest was byte-identical to base, as was the baseline file. Two sessions spent time on the conftest before anyone read the invocation, and one push has been blocked for hours over it. The conftest should also stop leaking, and that stays open as the other half of #16069. This half is narrower and comes first, because it is the half that produces the state at all. Fixing it here is not tolerating the leak guard: it stops manufacturing the condition the guard exists to report, in a shape that was never supported. Splitting keeps the common case at one invocation -- a changeset inside a single tree yields one empty group, which is skipped. Only a changeset spanning both backends runs twice, and that is precisely the case that previously could not pass at all. The guard runs the hook's OWN grep expressions rather than a copy, for the same reason the timeout tests (#15985) execute the helper instead of grepping for its message: a reimplementation agrees with itself and proves nothing. Four cases -- mixed, slm-only, other-only, and a lookalike path under repo_tests that must NOT be swept into the slm group, since the prefix is anchored and an unanchored matcher is how the commit-trailer re-derivation reported 3,444 violations. A fifth test is structural, because the two assignments can be correct while a stray combined pytest still runs beside them, and that combination would pass every behavioural test in the file. One harness bug caught in re-derivation and worth recording: the first version interpolated the path list with a Python repr, so bash received the characters backslash-n rather than newlines, every case collapsed to one line, and the harness passed while testing nothing. It feeds stdin now.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Closed
3 of 5 tasks
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
4 tasks
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.
Refs #16069
Thinking Path
The reported symptom is a leak:
autobot-slm-backend/conftest.pyleaves 54sys.moduleskeys installed while pytest works outside its directory, the guard blocks the push, and #15996 has been stuck behind it for hours. Two sessions looked at the conftest. It is 412 lines, its stubbing is deliberate and documented, and it is byte-identical to base — as issys_modules_leak_baseline.txt. A guard was failing a push over unchanged code.pytest.ini:64-73says why:CI obeys that. The pre-push hook did not —
tools/git-hooks/pre-push:259gathered every changed test into one$all_py_testsand ran a single pytest over it. A changeset touching both trees was therefore verified in a shape CI never runs andpytest.iniforbids, and the leak the guard reported is what that shape produces: the conftest stubs at import, sosqlalchemy,sqlalchemy.orm,sqlalchemy.ext.asyncio,modelsandmodels.databaseare stillMagicMocks whenrepo_testscollects afterwards.The guard was right, the conftest was innocent of this push, and the invocation was the defect.
What Changed
tools/git-hooks/pre-pushpartitions$all_py_testson the anchoredautobot-slm-backend/prefix and runs each non-empty group as its own pytest, aggregating failure and timeout across them.The common case is unchanged: a changeset inside one tree yields one empty group, which is skipped, so exactly one invocation runs. Only a changeset spanning both backends runs twice — the case that previously could not pass at all.
Failure semantics are preserved: any group failing sets
EXIT_CODE=1and prints its tail; any group hitting124routes todid_not_run, so a timeout still fails the push (#15985). A real failure outranks a timeout, since a timeout is "did not run" and a failure is information.Verification
The guard runs the hook's own grep expressions, extracted from the file — a reimplementation would agree with itself and prove nothing, the same reason the #15985 tests execute the helper rather than grepping for its message.
slm_py_testsother_py_testsrepo_tests/fixtures/autobot-slm-backend/decoy_test.py+ a real slm pathThe decoy row is the one worth having: the prefix is anchored, and an unanchored matcher is exactly how the commit-trailer re-derivation reported 3,444 violations (
RATCHET_BASELINES.mdrule 5).A fifth test is structural rather than behavioural, because the two assignments can be correct while a stray combined
pytest $all_py_testsstill runs beside them — and that combination would pass every behavioural test in the file.A harness bug caught by re-deriving rather than by reading. The first version interpolated the path list with a Python
repr, so bash received the two characters\ninstead of newlines, every case collapsed to a single line, and the harness passed while testing nothing. It feeds stdin now. Recording it because it is the same shape as the defect above: a check that ran, reported success, and measured something other than what it named.Model Used
Claude Opus 5 (1M context).
Correction, and then its correction — the claim stands, on different evidence
I asserted this unblocks #15996, then withdrew it, then restored it. The round trip is worth keeping because the withdrawal was a correct measurement of the wrong interval, which is subtler than being wrong.
The withdrawal.
gh pr diff 15996 --name-onlyreturns four files — three.claude/hooks/*.shand onerepo_tests/*_test.py— with no path underautobot-slm-backend/. Partition that and the slm group is empty, so the combined-session leak cannot arise. That reasoning is sound and the measurement is accurate.Why it was the wrong interval. The hook does not diff the PR against base.
tools/git-hooks/pre-push:112:diff_range="${remote_sha}..${local_sha}"That is the push range — what this push introduces relative to the remote branch head — not the net change against
Dev_new_gui. The blocked branch carries a base merge, so relative to its remote head the push introduces everything base has gained since, includingautobot-slm-backend/conftest.pyitself and an slm test file. The hook's ownrunning pytest on:line confirms it:autobot-slm-backend/slm/agent/health_collector_unit_states_16019_test.pyalongside threerepo_testsfiles the PR never touched.So an slm path genuinely enters the target set, one invocation spans both trees, #13084 fires, and the conftest leaks. This partition is the fix for that, not an adjacent improvement.
Two ranges, both correct, answering different questions:
origin/Dev_new_gui...HEADremote_sha..local_shaNothing in either output announces which question it answers, which is why substituting one for the other felt like a decisive check rather than a category error.
A consequence worth noting: the longer such a push stays blocked, the more base its range accumulates, and the likelier an slm path is in it. That is #16015's starvation loop in miniature — the block makes itself more likely to persist.
Single-issue rationale
This is one half of #16069 and deliberately does not close it. The other half — making the conftest stop leaking — is a refactor of shared SLM test infrastructure with live risk to that suite, and it carries an acceptance criterion this change cannot satisfy: every green previously obtained against the mocked
sqlalchemy/modelshas to be re-earned, not assumed. There is one confirmed instance already (a test on #16027 that passed vacuously against the unspeccedMagicMock), so the number found by intuition is a lower bound on the number that exist. Landing the invocation fix first unblocks the pushes that are stuck today without granting the leak any permanence, and without pretending the sweep has been done.Second, independent reproduction — my own push, a different branch
Not the branch that motivated this. #15980 is a dependabot PR that needed a base merge to pick up a floors fix. The merge is clean, the parity divergence it was red for is resolved, and the push is refused:
The two ranges, measured on that branch:
remote_sha..local_sha— what the hook diffsorigin/Dev_new_gui...HEAD— what the PR changesBoth trees in one target list, one pytest invocation, #13084 fires, the conftest's stubs are live while
repo_testscollects, the leak guard refuses the push. Exactly the mechanism described above, on a branch with no relationship to the first case.The base merge is what does it, which is why this will keep happening. Any branch behind base that merges to catch up pulls base's recent
autobot-slm-backend/*_test.pyfiles into its push range — and base gained several today. The condition is not rare and is not specific to one author.Two PRs are now blocked on this: #15996 and #15980. Neither can push without
--no-verifyorAUTOBOT_SYSMODULES_GUARD=off, and both of those trade a real guard for a green, which is the wrong trade twice over — the guard is correct and the invocation is what is wrong.