Skip to content

CI: mock Scryfall's deck-export endpoint, the last live-network call in test_valid_url - #663

Merged
WilfordGrimley merged 1 commit into
masterfrom
fix/ci-scryfall-deck-export-live-network
Jul 30, 2026
Merged

CI: mock Scryfall's deck-export endpoint, the last live-network call in test_valid_url#663
WilfordGrimley merged 1 commit into
masterfrom
fix/ci-scryfall-deck-export-live-network

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Why master is red, and why it is not the monolith

master at f947346b (PR #660, the monolith) fails Backend tests: 2 failed, 3542 passed, 11 skipped. Both failures are TestMTGIntegration::test_valid_url[scryfall] and [scryfall_with_www]. Neither touches any pipeline code. The monolith is not implicated, and the first monolith run is not compromised by this.

Scryfall.retrieve_card_list fetches https://api.scryfall.com/decks/<id>/export/text for real. CI received an HTML document instead of the text export — <!DOCTYPE html> appears verbatim in the base.py:64 "Invalid response" log line — so query_import_site raised InvalidURLException.

Why a retry is not the fix

The endpoint is healthy. A direct curl from outside CI returns HTTP 200, text/plain, and the expected body. The preceding master commit (1d433dd6) went green on the identical test 111 minutes earlier, and nothing between the two touches integrations.

What CI got was an edge/bot-challenge interstitial served to the GitHub Actions runner's address space — a property of where the request originates, not of the code or of Scryfall's contract. The next runner sits in the same address space, so a re-run is a coin flip rather than a remedy. (A re-run was kicked off in parallel with this PR purely to unblock deploy.sh faster if it happens to land green; it does not remove the need for this change.)

The fix is the one this file already established, twice

tappedout.net (2026-07-20) and manastack.com (2026-07-22) each hit this same class of live-network false-red, and each was mocked in place rather than skipped, so the site's own retrieve_card_list parsing stays covered and only the transport is faked. Scryfall now gets the same treatment. It was the last un-mocked flaky caller left in this parametrize.

Two things that are easy to get wrong here, both handled and both commented in place:

  • The host is api.scryfall.com, not Scryfall.get_host_names(). The two existing mocks build their patterns from get_host_names() because those sites fetch from the host they are matched on. Scryfall does not: it matches the scryfall.com deck page a user pastes, but requests netloc="api.scryfall.com". A pattern derived from get_host_names() would match nothing and leave the test exactly as red.
  • The mock is scoped to /decks/. api.scryfall.com also serves this suite's bulk-data, DFC and meld calls; a host-wide mock would gut test_get_double_faced_card_pairs / test_get_meld_pairs. real_http=True still carries every other Scryfall request to the real API unchanged.

The mocked body is the real response captured byte for byte (od -c verified: CRLF line endings, no trailing newline), so retrieve_card_list's own "// Sideboard" strip runs on genuine input and the recorded snapshot is unchanged.

Verification — mutation red, restore, green

step result
baseline before change, local test_valid_url[scryfall] passes — this box's IP is not the one being challenged, which is itself the evidence the failure is origin-dependent, not code-dependent
mutate mocked body "3 Past in Flames""3 MUTANT" 2 failed (scryfall, scryfall_with_www) on the snapshot comparison
restore 13 passed, 2 skipped, 13 snapshots passed

The mutation is not in the class that survives by asserting a value both paths reach: had the mock fallen through to the real network, altering the mock body could not have changed the outcome. Red-on-mutation therefore proves the mock is genuinely serving the request and that the snapshot assertion is live.

black --check, ruff, isort, mypy all pass (pre-commit).

Scope

One test file, 36 added lines, no production code. Deliberately shipped alone: a red-master fix should be reviewable and mergeable without accepting any behaviour change alongside it.

🤖 Generated with Claude Code

https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN

…in test_valid_url

master went RED at f947346 (PR #660, the monolith) on
test_valid_url[scryfall]/[scryfall_with_www] - 2 failed, 3542 passed. The failure is
NOT in the monolith and touches no pipeline code: it is the third instance of a failure
mode this test already documents twice.

WHAT HAPPENED. `Scryfall.retrieve_card_list` fetches
`https://api.scryfall.com/decks/<id>/export/text` for real. CI received an HTML document
instead of the text export - `<!DOCTYPE html>` appears verbatim in the `base.py:64`
"Invalid response" log line - so `query_import_site` raised `InvalidURLException`.

WHY THIS IS NOT A SCRYFALL OUTAGE, AND WHY A RETRY IS NOT THE FIX. The endpoint is
healthy: a direct curl from outside CI returns HTTP 200, `text/plain`, and the expected
body. The immediately preceding master commit (1d433dd) went green on the identical
test 111 minutes earlier, and nothing between the two touches integrations. What CI got
was an edge/bot-challenge interstitial served to the GitHub Actions runner's address
space - a property of WHERE the request originates, not of the code or of Scryfall's
contract. The next runner sits in the same address space, so a re-run is a coin flip,
not a remedy.

THE FIX IS THE ONE THIS FILE ALREADY ESTABLISHED. tappedout.net (2026-07-20) and
manastack.com (2026-07-22) each hit this same class of live-network false-red and each
was mocked in place rather than skipped, so the site's own `retrieve_card_list` parsing
stays covered and only the transport is faked. Scryfall now gets the same treatment,
and it was the last un-mocked flaky caller left in this parametrize.

TWO THINGS THAT ARE EASY TO GET WRONG HERE, both handled and both commented in place:

  - THE HOST IS `api.scryfall.com`, NOT `Scryfall.get_host_names()`. The two existing
    mocks build their patterns from `get_host_names()` because those sites fetch from
    the host they are matched on. Scryfall does not: it matches the `scryfall.com` deck
    page a user pastes but requests `netloc="api.scryfall.com"`. A pattern derived from
    `get_host_names()` would match nothing and leave the test exactly as red.

  - THE MOCK IS SCOPED TO `/decks/`. `api.scryfall.com` also serves this suite's
    bulk-data, DFC and meld calls; a host-wide mock would gut
    `test_get_double_faced_card_pairs`/`test_get_meld_pairs`. `real_http=True` still
    carries every other Scryfall request to the real API unchanged.

The mocked body is the real response captured byte for byte (od -c verified: CRLF line
endings, no trailing newline), so `retrieve_card_list`'s own `"// Sideboard"` strip runs
on genuine input and the recorded snapshot is unchanged.

VERIFICATION (mutation red -> restore -> green, local, python 3.10):
  - baseline, before this change: test_valid_url[scryfall] passes locally, because this
    box's IP is not the one being challenged - which is itself the evidence that the
    failure is origin-dependent rather than code-dependent.
  - mutate the mocked body ("3 Past in Flames" -> "3 MUTANT"): 2 failed
    (scryfall, scryfall_with_www) on the snapshot comparison. This proves the mock is
    genuinely serving the request - had it fallen through to the real network, altering
    the mock body could not have changed the outcome - and that the snapshot assertion
    is live rather than vacuous.
  - restore: 13 passed, 2 skipped, 13 snapshots passed.
  - black --check: unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
@WilfordGrimley
WilfordGrimley merged commit b37eb95 into master Jul 30, 2026
14 checks passed
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