Skip to content

test(quality): ten tests that could not fail — bare 'except Exception' swallowing AssertionError, and one asserting imports it never performed #15189

Description

@mrveiss

Found while converting live-service drivers for #14979, in a file that conversion never touched.

What

autobot-backend/cache/cache_consolidation_p4_test.py — all ten test functions were structurally incapable of failing. Every one was shaped:

try:
    ...
    assert ...
    return True
except Exception:
    return False

A bare except Exception catches AssertionError. So a failing assertion returned False instead of propagating, and pytest treats a returning test as passing (with a Did you mean to use assert instead of return? warning, which is how it was noticed). Ten green tiles, zero enforceable assertions.

One was worse than vacuous — it was a false claim. test_migrated_files_import had a body of pass plus six print() statements announcing that six modules import successfully. The import statements had been stripped at some point. It asserted six imports while performing none, and reported success for all six.

Status

Fixed in PR #15166: the file goes 499 → 236 lines, 10 passed with 0 return-warnings, every original assertion preserved and now able to fail, roughly 30 forbidden print() calls removed, and test_migrated_files_import rewritten to drive importlib.import_module and assert on the result. Unwrapping the swallow surfaced no hidden failure — the vacuousness was structural, not concealing a real break.

This issue exists because the fix belongs to a PR about something else, and this deserves its own record.

Why it is worth an issue rather than a line in a changelog

This is the failure mode the whole wave-0 cluster is about, in its purest form: a suite reporting success while asserting nothing. It is the same class as #15182 (assert True in a test the marker run actually selects) and the same class as #15018 (a coverage guard whose enumeration matched half of nothing). The distinguishing feature here is that the tests looked thorough — they had real assertions inside them, and the swallow was three lines away.

The open question is how many more there are. pytest's return-not-None warning is emitted, not fatal, so it scrolls past in a green run.

Acceptance criteria

  • Sweep the repository for the try: … assert …; return True / except Exception: return False shape and report the count. Ten in one file is not evidence there is one file.
  • Sweep separately for the weaker form — any test function that returns a value — since pytest reports that as a warning and it is the cheapest possible detector for this class.
  • Make the detector fatal rather than advisory: a returning test function fails CI. If that cannot be turned on immediately because of an existing population, record the population as a down-only ratchet so it can only shrink.
  • Sweep for the second shape too: a test whose body is pass or only print() while its name and output claim an assertion. That one cannot be caught by the return-warning and needs its own check.
  • The contrast mutation for whatever guard lands: reintroduce a swallowing test and show CI fails.
  • Confirm cache_consolidation_p4_test.py was not in any exemption or skip list that would have suppressed the warning, and record the answer either way.

Refs #14979, #15166, #15182, #15018.

Activity

  1. mrveiss commented on Aug 28, 2026

    @mrveiss
    OwnerAuthor

    Correcting this issue's premise — the remedy I proposed would have caught one of the ten

    I wrote above that "the detector already existed and was ignored because it was advisory", and set an acceptance criterion to make pytest's return-not-None warning fatal. That framing is wrong, and the criterion as written is insufficient.

    There is already a fatal guard for exactly this defect: repo_tests/tests_that_return_instead_of_asserting_test.py (#14920), an AST sweep with a per-tree down-only ratchet. It ran, it gates, and it did not catch nine of the ten — for a structural reason:

    if _own_nodes(function, (ast.Assert, ast.Raise)):
        continue

    The exclusion treats an assert that is present as an assert that can fire. Under a bare except Exception, it cannot. Nine of the ten functions contained asserts — inert ones — so the sweep skipped them. Measured against the base file, it attributed only 2 offending returns in the whole module, both inside test_migrated_files_import, the one function with no assert at all:

    BASE offending in that file: 2   [('test_migrated_files_import', 288), (…, 281)]
    NOW  offending in that file: 0
    

    So the shape was worse than neglected. It tripped PytestReturnNotNoneWarning at runtime and read as defended to the AST guard. Both detectors agreed the file was fine — one because it only warns, the other because it mistook a present assertion for an effective one.

    That is the real finding, and it is filed as #15195, with the concrete rule: an assert is not protective when it sits under a handler catching Exception, BaseException or AssertionError without re-raising.

    What this changes here

    I filed this issue on the strength of a report I did not check against the guard's source. The agent that found it corrected me, and the corrected version is a better finding than the one I filed.

    Refs #14920, #15195, #15166.

  2. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    Two of the three remaining sites cleared. 1c7688e9ff (PR #15393), verified against merged Dev_new_gui: _KNOWN_OFFENDERS["autobot-backend"] is now 75 (was 86) and _SWALLOWED_ASSERTIONS["autobot-backend"] is 4 (was 5). Zero except Exception remain in config_consolidation_p2_test.py — the single textual match on merged base is the module docstring describing the old pattern.

    What came out from under the swallow is worse than the swallow itself. Ten checks lived in one function, each wrapped try: assert ...; except Exception: return False. Removing that revealed:

    • Test 2 never tested anything, and took eight others with it. It called unified_config_manager._get_default_config() — a method that does not exist on that object; the real one is module-level config.defaults.get_default_config(). The AttributeError was caught and the function returned, so tests 3 through 10 never executed at all while the suite reported PASSED. One non-existent method call silently disabled nine tests.
    • Test 4 wrote a live config/test.json into the repository root on every run — ConfigManager.config_dir resolves from PATH.PROJECT_ROOT, which is the repo root, not autobot-backend/. A test mutating the working tree, on a machine that also hosts a CI runner. Now redirected via monkeypatch + tmp_path.
    • Test 7 was vacuous independently of the swallow — it printed whichever env var happened to be set and asserted nothing either way. Now a real round trip through config.loader.apply_env_overrides.
    • Test 8 asserted a stale pre-fix value (0.8), where bug(multimodal): VoiceProcessor reads multimodal.audio but defaults define multimodal.voice — config silently ignored #13207 deliberately set 0.7.

    The ten are now independent top-level tests, so one failure can no longer suppress the rest — which was the actual defect, more than the bare except.

    One flake was caught in review and fixed before merge: the env-override test asserted strict dict equality against the entire apply_env_overrides result, which would fail whenever any of ~15 other AUTOBOT_* vars happened to be set — sourcing .env.localhost is a documented workflow here. Now scoped to the key it controls.

    Not closing. The four test_causal_framework_integration.py aggregator-consumed *_full_pipeline methods remain — 1145 lines, grandfathered in python_file_size_known_large.py, needing #15166-style restructuring. That is its own scope, and the guard infrastructure to catch regressions is already in place from #15254/#15241/#15256/#15351.

  3. mrveiss commented on Sep 2, 2026

    @mrveiss
    OwnerAuthor

    Closing — all six criteria met on merged base, verified against git show origin/Dev_new_gui:.

    AC evidence
    1 — sweep for the try: assert…; return True / except Exception: return False shape and report the count repo_tests/tests_that_return_instead_of_asserting_test.py:266 _SWALLOWED_ASSERTIONS, enforced by test_no_test_smothers_its_own_assertions_under_a_swallowing_handler at :428. Ten in one file was indeed not the whole population.
    2 — sweep separately for the weaker form, any test function returning a value test_no_tree_outside_the_known_set_has_a_test_that_returns_a_value at :349.
    3 — make the detector fatal, or record the population as a down-only ratchet _KNOWN_OFFENDERS at :139, enforced by test_the_known_offender_budgets_only_ever_shrink at :369.
    4 — sweep for the second shape: a body that is pass or only print() repo_tests/tests_that_execute_nothing_test.py, driving empty_bodies_in from repo_tests/collected_test_model.py:406-423.
    5 — contrast mutation: reintroduce a swallowing test, show CI fails test_the_detector_finds_a_planted_return_and_spares_the_legitimate_ones at :498 — the detector is run against a planted offender and against legitimate tests, so it proves both directions rather than only that something fails.
    6 — confirm cache_consolidation_p4_test.py was in no exemption or skip list, and record the answer either way Answer: it was not. No filterwarnings entry exists in pytest.ini, setup.cfg, pyproject.toml or autobot-backend/pytest.ini — the count is 0 in all four. Its only references anywhere are .test_durations (a timing record), scripts/migrate_tests.py (a path map), and the two guards' own changelog comments. So the warning was emitted normally and was simply non-fatal; nothing suppressed it.

    Two things beyond the checklist that make the guard non-vacuous, and are worth noting because this issue is about checks that cannot fail: a population floor at :330 (_MIN_MODULES = 1800, _MIN_TEST_FUNCTIONS = 25000) so a collapsed sweep fails by name instead of reading clean, and test_an_unparseable_swept_file_fails_loudly_instead_of_reading_as_clean at :578 so a file the sweep cannot parse is not silently counted as compliant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions