Skip to content

--block-network makes the whole test suite unrunnable on Windows #44

Open

Description

@PerryLink

pyproject.toml sets addopts = "--block-network -p no:tach", and on Windows that stops the
suite before a single test runs — not a few failures, the entire run.

Mechanism

pytest-recording implements --block-network by replacing socket.socket.connect. Windows has no
AF_UNIX socketpair(), so socket.socketpair() falls back to _fallback_socketpair, which opens a
listening socket on 127.0.0.1 and connect()s to it. asyncio's ProactorEventLoop builds its
self-pipe with exactly that call, so the loop cannot be constructed while the guard is active:

asyncio/runners.py  -> events.new_event_loop()
asyncio/windows_events.py:316 -> super().__init__(proactor)
asyncio/proactor_events.py:781 -> self._make_self_pipe()
asyncio/proactor_events.py:635 -> self._ssock, self._csock = socket.socketpair()
socket.py:629 (in _fallback_socketpair) -> csock.connect((addr, port))
pytest_recording/network.py:124 -> raise RuntimeError("Network is disabled")

So asyncio.run() itself raises, which is why every async test errors during setup rather than
asserting anything. On Linux socketpair() goes through the C-level _socket.socketpair() and never
calls Python connect(), so nothing surfaces there — and since publish.yml only smoke-tests the
built distributions (check_installed_distribution.py), no CI runs pytest on any platform.

Measured on Windows, Python 3.14.7, uv sync --all-extras, no extra flags

uv run pytest -q result
before 201 failed, 223 passed, 64 errors
after the one-line change below 8 failed, 416 passed

The change

allowed_hosts is one of the keys this repo's own vcr_config fixture already owns, and it is matched
by re.match against the connect target — so only loopback comes back, and provider endpoints stay
refused. In tests/conftest.py:

     return {
+        # --block-network (see addopts) blocks every socket connect, which on Windows
+        # takes the suite down before a single test runs: there the ProactorEventLoop
+        # builds its self-pipe with socket.socketpair(), whose Windows fallback
+        # connects to 127.0.0.1, so asyncio.run() itself raises "Network is disabled".
+        # Loopback is the only host let back through; provider endpoints stay blocked.
+        "allowed_hosts": ["127.0.0.1"],
         # The registered JSON serializer keeps HTTP bodies as nested objects, so the
         # cassette diffs like its payloads instead of escaped string literals.
         "serializer": "json",

It is deliberately not a change to addopts: the flag stays exactly as it is, and no test is touched.

Why allowing loopback does not weaken the guard

Verified directly, with the change in place: a loopback connect succeeds, while
socket.socket().connect(("example.com", 80)) still raises RuntimeError: Network is disabled.

One thing I did not touch

8 tests in tests/test_client_with_live_apis.py still fail after this — all of them the -anthropic
variants, failing inside VCR's httpx stub rather than at loop construction. They are visible once the
loop can be built, they look like a separate cassette-vs-SDK-version mismatch, and I have left them
alone rather than fold an unrelated change into this report.

I have the change on a branch (PerryLink:fix/windows-block-network, the 7 added lines above, one file),
but opening a pull request here is refused for this account — createPullRequest returns
FORBIDDEN and viewerPermission is READ — so an issue is the only channel I have. The branch is
there if you would rather take the diff from it than retype it.

Activity

  1. rmnvg commented on Sep 22, 2026

    @rmnvg

    Confirming @PerryLink's diagnosis and fix — traced through the same mechanism (Windows socket.socketpair() fallback calling connect(), which ProactorEventLoop._make_self_pipe() hits on every asyncio.run(), tripping pytest-recording's guard before any async test body runs).

    Applied the exact one-line change (adding "allowed_hosts": ["127.0.0.1"] to the vcr_config fixture in tests/conftest.py) and ran the full suite on macOS: 424 passed, no regressions — expected, since socketpair() doesn't go through connect() outside Windows, so this is a no-op there and only matters on the platform where it's currently broken.

    Like @PerryLink, I don't have write access to open a PR against this repo (createPullRequest is forbidden for non-collaborators here). The change is on a branch on my fork if a maintainer wants to pull it directly instead of retyping it:

    https://github.com/rmnvg/system-one-adapter-python/tree/fix/windows-block-network

    Diff: rmnvg@2f98b76

         return {
    +        # --block-network (see addopts) blocks every socket connect, which on Windows
    +        # takes the suite down before a single test runs: there the ProactorEventLoop
    +        # builds its self-pipe with socket.socketpair(), whose Windows fallback
    +        # connects to 127.0.0.1, so asyncio.run() itself raises "Network is disabled".
    +        # Loopback is the only host let back through; provider endpoints stay blocked.
    +        "allowed_hosts": ["127.0.0.1"],
             # The registered JSON serializer keeps HTTP bodies as nested objects, so the
             # cassette diffs like its payloads instead of escaped string literals.
             "serializer": "json",
  2. aoright commented on Sep 24, 2026

    @aoright

    I verified this fix against latest main (e1d4cc9, post-v0.2.1) on fork branch fix/windows-block-network (commit fd09780).

    Compare: main...aoright:system-one-adapter-python:fix/windows-block-network

    Summary

    1. Configured allowed_hosts: ["127.0.0.1", "localhost"]:
      • Including both IPv4 loopback (127.0.0.1) and hostname (localhost) ensures Windows ProactorEventLoop._make_self_pipe and any local test harness connects proceed unhindered.
      • External provider endpoints remain strictly blocked by --block-network, preserving the offline cassette replay guarantee.
    2. Verification on v0.2.1 suite:
      • uv run pytest: 424 passed.
      • uv run pyrefly check: 0 errors.
      • uv run ruff check tests/conftest.py: clean (0 findings).
  3. PerryLink commented on Sep 25, 2026

    @PerryLink
    Author

    Thanks @rmnvg and @aoright — the independent runs are the useful part here. macOS 424 passed is what the mechanism predicts (nothing on the self-pipe path goes through connect() off Windows, so the change is a no-op there), and @aoright's run against e1d4cc9 shows it still applies after v0.2.1.

    I re-checked main today: pyproject.toml still sets addopts = "--block-network -p no:tach", and vcr_config in tests/conftest.py still returns no allowed_hosts. So the suite is still unrunnable on Windows exactly as reported — the fix is still live, not stale.

    On the one difference between the two branches — @aoright added "localhost" next to "127.0.0.1". Both work, but only the first is needed for this bug. _fallback_socketpair binds ("127.0.0.1", 0) and connects to the address it gets back, so the host it connects to is the literal 127.0.0.1. pytest-recording matches via re.match over the entries joined into one alternation (pytest_recording/network.py:164-168), so "localhost" only adds coverage for code that connects by name. Harmless to keep; if a minimal diff is preferred, one entry is enough.

    Worth knowing for anyone extending this list later: because the entries are joined into a single pattern and matched with re.match rather than compared for equality, each entry is a regex, and . matches any character. Fine for 127.0.0.1, but a future entry is a pattern, not a literal.

    Still untouched, as noted in the report: the 8 -anthropic cases in tests/test_client_with_live_apis.py fail inside VCR's httpx stub rather than at loop construction, which is why they survived this change. Leaving them for whoever owns the cassettes.

  4. simin-yuan commented on Nov 2, 2026

    @simin-yuan

    Fixed upstream in pytest-recording 0.14.1 via kiwicom/pytest-recording#213.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions