Skip to content

test(npu_pipeline): test_dispatcher.py's async mock_post breaks dispatcher.py's sync-call contract #13004

Closed

Description

@mrveiss

What

Discovered while verifying issue #12979 batch 9 (unrelated scope — services/npu_pipeline/dispatcher.py's HTTP session is a documented RAW carve-out in that batch, not converted). Confirmed pre-existing via a cp-swapped baseline against origin/Dev_new_gui — identical failures with the unmodified file.

autobot-backend/services/npu_pipeline/test_dispatcher.py fails 5/5 of its tests when run standalone:

FAILED test_happy_path_three_workers
FAILED test_worker_drop_mid_pass_failover
FAILED test_single_worker_produces_tokens
FAILED test_worker_drop_no_peer_raises
FAILED test_context_manager_closes_session

Root cause

  • services/npu_pipeline/test_dispatcher.py:78, :175, :198 — mock_post is declared async def mock_post(url, json=None): ... and assigned as session_mock.post = mock_post. Production code (dispatcher.py:151, :193) calls it synchronously — async with self._session.post(url, json=payload) as resp: — expecting .post(...) to return an async context manager directly, not a coroutine. Calling an async def function returns a coroutine object without executing its body, so:
    • test_happy_path_three_workers, test_single_worker_produces_tokens: TypeError: 'async for' requires an object with __aiter__ method, got coroutine (the coroutine is passed to async with/async for unexecuted).
    • test_worker_drop_no_peer_raises: the raise inside mock_post never executes (coroutine body doesn't run until awaited), so the expected WorkerDropError chain doesn't fire as designed; observed failure is TypeError: Expected a BaseException type, but got 'MagicMock' in the pytest.raises(WorkerDropError, ...) context — WorkerDropError itself resolves to a MagicMock, which also implicates the module-loading indirection below.
  • services/npu_pipeline/test_dispatcher.py:22-30 (_load_module) — the file bypasses sys.modules stubs the root conftest installs for services.npu_pipeline.*, but the reuse guard (if name in sys.modules and hasattr(sys.modules[name], "__file__")) can pick up a stub module rather than the freshly-loaded one depending on import order, which would explain WorkerDropError binding to a MagicMock attribute instead of the real exception class in test_context_manager_closes_session / test_worker_drop_no_peer_raises.

Evidence

$ python3 -m pytest autobot-backend/services/npu_pipeline/test_dispatcher.py -q
5 failed in 1.00s

Same 5 failures reproduce against origin/Dev_new_gui's unmodified dispatcher.py (cp-swap baseline, no diff).

Suggested fix

Change mock_post in each of the 3 affected tests from async def to a plain def returning the pre-built resp (or an AsyncMock with side_effect), matching the synchronous-call / async-context-manager-return contract HTTPClientManager-style clients use. Separately, audit _load_module's stub-reuse guard against the root conftest's services.npu_pipeline.* stubs.

Model Used

Sonnet (claude-sonnet-5)

Activity

  1. mrveiss commented on Sep 2, 2026

    @mrveiss
    OwnerAuthor

    Closing — the defect is gone from merged base, and every criterion is met. Verified against origin/Dev_new_gui @ cf2d29d54e.

    Claimed 3 async def mock_post breaking dispatcher.py's sync-call contract. Measured: 0 remain; 4 sync def mock_post.

    Criterion Evidence
    mock_post becomes a plain def returning the pre-built response autobot-backend/services/npu_pipeline/test_dispatcher.py:114, :162, :211, :234 — four sync def mock_post(url, json=None). git grep "async def mock_post" on that file → 0 hits.
    Audit _load_module's stub-reuse guard against the root conftest stubs :35 now reads if existing is not None and existing.__dict__.get("__file__"), probing __dict__ rather than hasattr. The reason is stated inline at :30-33: the PEP-562 catch-all __getattr__ on the conftest stubs makes hasattr unconditionally true, so the old guard could never distinguish a stub from a real module.
    WorkerDropError binds the real exception rather than a MagicMock :76 WorkerDropError = _dispatcher_mod.WorkerDropError, taken from the real-loaded module and used at :218 in pytest.raises(WorkerDropError, match="w0"). Against a MagicMock that raises would not have been meaningful.
    Beyond the checklist — the stub escape is closed _load_real_modules() at :46-68 snapshots _REAL_LOAD_KEYS and restores them in a finally, so the real modules it loads do not leak into later tests. Landed under #13361.
    The five tests run .test_durations:12226-12230 lists all five, so CI collects and runs them. A duration proves collection rather than a green result, but base CI is green and the sync contract the issue describes is now what the code does.

    Resolved by #13361 rather than by work under this number.

    Worth noting for anyone reading this issue later: the hasattr-vs-__dict__ detail at :30-35 is the same PEP-562 trap that #13234 is open about — there, _make_pkg_stub fabricates dunders so every getattr-based stub check silently returns a MagicMock. This file works around it locally; #13234 is the general fix.

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