Skip to content

fix(tests): conftest re-executed already-loaded modules, breaking isinstance across the suite (#12839) - #12851

Merged
mrveiss merged 2 commits into
Dev_new_guifrom
issue-12839
Jul 28, 2026
Merged

mrveiss merged 2 commits into
Dev_new_guifrom
issue-12839

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Closes #12839

Thinking Path

Two failing tests, and I assumed one cause. There were two independent defects — fixing the obvious one left both tests still red, which is what forced the second investigation.

Defect 1 — the KB mock lied about its own interface. ClaimClassifier._search_kb picks its branch with hasattr:

if hasattr(self.kb, "search_async"):   results = await self.kb.search_async(...)
elif hasattr(self.kb, "search"):       results = self.kb.search(...)

The fixture was a bare MagicMock(), which auto-creates every attribute, so hasattr(kb, "search_async") was True and the classifier awaited a plain MagicMock — object MagicMock can't be used in 'await' expression. The classifier caught that, logged it, and returned no results, so the test was quietly asserting against a degraded error path instead of the one it meant to exercise. spec=["search"] makes the probe answer honestly.

Defect 2 — the same module executed twice. Fixing the mock did not fix isinstance, so the duplicate-identity theory needed proof rather than assumption. A stack probe in knowledge_grounding_models.py during a real pytest run showed both executions:

### EXEC knowledge_grounding_models
  services/claim_classifier.py:31  from .knowledge_grounding_models import (

### EXEC knowledge_grounding_models
  conftest.py:1141  pytest_configure -> _real_load_light_services()
  conftest.py:1118  _real_load_and_bind(f"services.{name}", svc_path)

_real_load_and_bind re-executes the file and swaps a fresh module into sys.modules, while services.claim_classifier keeps holding the classes from the first execution. Hence isinstance(x, Claim) returning False for an object whose repr says it is a Claim, with only one sys.modules entry to inspect.

Re-execution exists solely to replace a MagicMock stub, so an already-real module loaded from that same path is now left alone. The parent bind is still (re)applied on that skip path — patch("pkg.mod.NAME") resolves via getattr(pkg, "mod"), and dropping it would resurrect the inert-patch bug from #11532/#11618. That case has its own test.

What Changed

  • conftest.py — _real_load_and_bind returns early when sys.modules[name].__file__ already matches the target path, after re-applying the parent bind.
  • tests/services/test_claim_classifier.py — KB fixture uses MagicMock(spec=["search"]).
  • tests/test_conftest_real_load_identity.py (new) — pins the loader invariant.

Verification

conftest.py is loaded by every backend test, so this was measured against a pristine origin/Dev_new_gui worktree using an identical command:

baseline: 14 failed, 5304 passed, 130 skipped
branch:   12 failed, 5310 passed, 130 skipped

failures only on this branch (regressions):  NONE
failures fixed:  test_claim_classifier.py::TestIndividualClassification::test_classify_claim_basic
                 test_claim_classifier.py::TestBatchClassification::test_batch_classify_single_claim

The 12 remaining failures are byte-identical on both trees (gitlab/gitea connector tests requiring a live Redis, etc.) and are untouched here. The +6 passes are the 2 fixed tests plus 4 new ones covering: an already-real module is not re-executed, an instance stays isinstance-valid across a repeat load, a stub is still replaced by the real module, and the parent bind is applied on the skip path.

A note on the first verification attempt, since it nearly produced a false conclusion: running tests/ llc/tests/ together reported "Interrupted: 49 errors during collection", which looked like this change breaking the suite. The identical command on the pristine baseline produced exactly the same 49 errors — pre-existing, triggered by combining those two paths in one invocation, and it meant nothing had actually executed. The numbers above come from a subset that collects cleanly on both trees.

Model Used

Claude Opus 5

… + spec the KB mock (#12839)

Two independent defects behind the two failing claim-classifier tests.

1. Duplicate module identity. `services.claim_classifier` imports
   `.knowledge_grounding_models` during collection; conftest's
   `_real_load_light_services()` then re-executed the SAME file via
   `_real_load_and_bind`, building a second set of class objects and swapping
   them into sys.modules while claim_classifier kept referencing the first.
   Result: `isinstance(result, Claim)` False against an object whose repr says
   it IS a Claim. Confirmed with a stack probe showing both executions.

   Re-execution exists only to replace a MagicMock stub, so a module already
   real-loaded from that same path is now left alone — the parent bind is still
   (re)applied, since patch("pkg.mod.NAME") resolves via getattr(pkg, "mod")
   and skipping it would resurrect the inert-patch bug from #11532/#11618.

2. The KB fixture was a bare MagicMock(), which auto-creates every attribute.
   `ClaimClassifier._search_kb` prefers `search_async` when `hasattr` says it
   exists, so the classifier awaited a plain MagicMock ("object MagicMock can't
   be used in 'await' expression"), caught it, and returned no results — the
   test was asserting against a degraded path rather than the one it meant to
   exercise. `spec=["search"]` makes the hasattr probe answer honestly.

Verification (conftest is loaded by every backend test, so measured against a
pristine origin/Dev_new_gui worktree with an identical command):

  baseline: 14 failed, 5304 passed, 130 skipped
  branch:   12 failed, 5310 passed, 130 skipped

  failures only on this branch (regressions): NONE
  failures fixed: the two test_claim_classifier cases

The remaining 12 failures are identical on both trees (gitlab/gitea connector
tests needing a live Redis, etc.) and are untouched by this change. +6 passes =
2 fixed + 4 new tests pinning the loader invariant: an already-real module is
not re-executed, an instance stays isinstance-valid across a repeat load, a
stub IS still replaced, and the parent bind is applied on the skip path.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.
@mrveiss
mrveiss merged commit 8dcf6f0 into Dev_new_gui Jul 28, 2026
37 checks passed
@mrveiss
mrveiss deleted the issue-12839 branch July 28, 2026 01:18
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