Spaces:
Sleeping
Sleeping
avoigt1121 Claude Fable 5 commited on
Commit ·
b676112
1
Parent(s): 99e8ddc
verify: routing leg passes; dispatch blocked by private-Space 404 (HF_TOKEN unset)
Browse filesSigned-in test against the orchestrator dev Space: it correctly classified a DNA
question and routed to PDAC Genomics Agent. Dispatch then failed with a repo-lookup
404 because this Space is private and the orchestrator sends no token — consistent
with the caller-side HF_TOKEN root cause. Nothing to fix in this repo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TODO.md +33 -0
- memory.md +109 -0
- tests/conftest.py +109 -0
- tests/test_hermetic.py +69 -0
TODO.md
CHANGED
|
@@ -202,6 +202,39 @@ the design doc's M0–M4 is the intent behind it — see that doc's §0).
|
|
| 202 |
statistical-honesty stance (no cross-gene correction, `testable=false` + reason, small-n
|
| 203 |
caveats), and metadata-label vs live-PurIST provenance. No behaviour change.
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
## Open questions
|
| 206 |
|
| 207 |
- ~~HF account / Space name~~ **RESOLVED 2026-07-24** — Space `anne-voigt/pdac-genomics-agent` created
|
|
|
|
| 202 |
statistical-honesty stance (no cross-gene correction, `testable=false` + reason, small-n
|
| 203 |
caveats), and metadata-label vs live-PurIST provenance. No behaviour change.
|
| 204 |
|
| 205 |
+
## Test-suite hermeticity ✅ (2026-07-28)
|
| 206 |
+
|
| 207 |
+
- [x] **Enforce "no live network" instead of asserting it** — session-wide socket guard in
|
| 208 |
+
`tests/conftest.py` (module scope, so it covers *collection*; an autouse fixture would run
|
| 209 |
+
too late for import-time leaks). Non-loopback `connect`/`connect_ex`/`create_connection`
|
| 210 |
+
raise, naming the host. Loopback + AF_UNIX allowed.
|
| 211 |
+
- [x] **Close the last hole: the three UI modules.** `test_ui_handler_gating` /
|
| 212 |
+
`test_deploy_surface` / `test_machine_api_auth` each `import gradio_ui`, whose Blocks graph
|
| 213 |
+
is built at module scope; off a Space, gradio's `attach_oauth` mocks the login button by
|
| 214 |
+
calling HF `whoami`. conftest stubs `gradio.oauth._get_mocked_oauth_info` — the narrowest
|
| 215 |
+
seam, and **test-only: `gradio_ui.py` is unchanged**, so the module-level `demo` that HF
|
| 216 |
+
Spaces launches still works. Restructuring `gradio_ui` was considered and rejected as
|
| 217 |
+
disproportionate.
|
| 218 |
+
- [x] **`tests/test_hermetic.py`** — the guard is itself tested (installed / refuses / message
|
| 219 |
+
names the host / loopback still works / all three UI modules import). Without this the guard
|
| 220 |
+
decays into the same unchecked docstring it replaced.
|
| 221 |
+
- [x] **Verified: 143 pass with sockets blocked outright, no `--ignore`** (was 111 with the three
|
| 222 |
+
UI modules ignored). Mutation-tested — reverting the `study_metadata` patch reproduces the
|
| 223 |
+
original bug and the guard catches it. Runtime 22s → 4s.
|
| 224 |
+
- [x] **CI no longer needs an HF login** (previously those three modules could not import without
|
| 225 |
+
an HF token at all). Verified with `HF_TOKEN` unset.
|
| 226 |
+
- [ ] **Watch on gradio upgrades:** the stub targets gradio's private
|
| 227 |
+
`_get_mocked_oauth_info`. If a bump renames it, `test_hermetic.py`'s UI-import test fails —
|
| 228 |
+
that failure is the signal, not a flake. Re-point the stub.
|
| 229 |
+
- [ ] Optional: promote the stub-vs-restructure choice to an ADR if it proves contested.
|
| 230 |
+
**Next free is 0008** — `main` has 0006 (variant×subtype join) *and* 0007 (re-curation
|
| 231 |
+
cadence) published. Claim in `RESERVED.md` on `main`, alone, first.
|
| 232 |
+
- [ ] ⚠️ **This branch's `docs/adr/RESERVED.md` is stale — it stops at 0005 and says "next free
|
| 233 |
+
0006", both of which are wrong.** It is the per-lane-copy failure the ledger's own postmortem
|
| 234 |
+
describes: the shared-row conflict cannot fire between branches, so `ls docs/adr/` and the
|
| 235 |
+
local ledger will both hand you a burned number. Check with
|
| 236 |
+
`git show main:docs/adr/RESERVED.md` before claiming, and rebase this branch.
|
| 237 |
+
|
| 238 |
## Open questions
|
| 239 |
|
| 240 |
- ~~HF account / Space name~~ **RESOLVED 2026-07-24** — Space `anne-voigt/pdac-genomics-agent` created
|
memory.md
CHANGED
|
@@ -839,6 +839,7 @@ by running the suite with `socket.connect` blocked outright — **111 tests pass
|
|
| 839 |
all** (the three UI test modules still touch it, but that is Gradio's mocked-OAuth HF call at
|
| 840 |
import in a local dev env, not our code and not present on a Space).
|
| 841 |
|
|
|
|
| 842 |
|
| 843 |
---
|
| 844 |
|
|
@@ -865,3 +866,111 @@ misled again. One contract, with instructions when you miss it.
|
|
| 865 |
Ordering is gate-first: an unauthenticated caller gets `denied` and learns nothing about our
|
| 866 |
inputs — pinned by `test_argument_errors_are_reported_only_after_the_auth_gate`. 132 tests (7 new,
|
| 867 |
alongside the denial-diagnosis guards they mirror).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
all** (the three UI test modules still touch it, but that is Gradio's mocked-OAuth HF call at
|
| 840 |
import in a local dev env, not our code and not present on a Space).
|
| 841 |
|
| 842 |
+
<<<<<<< HEAD
|
| 843 |
|
| 844 |
---
|
| 845 |
|
|
|
|
| 866 |
Ordering is gate-first: an unauthenticated caller gets `denied` and learns nothing about our
|
| 867 |
inputs — pinned by `test_argument_errors_are_reported_only_after_the_auth_gate`. 132 tests (7 new,
|
| 868 |
alongside the denial-diagnosis guards they mirror).
|
| 869 |
+
=======
|
| 870 |
+
---
|
| 871 |
+
|
| 872 |
+
## 2026-07-28 — Hermetic is now *enforced*, and the whole suite is hermetic
|
| 873 |
+
|
| 874 |
+
Finishes the item above. That fix patched the one hole that had been found; this one removes the
|
| 875 |
+
reason holes could survive — and closes the last remaining one.
|
| 876 |
+
|
| 877 |
+
**The lesson being encoded:** the "NO live network" claim lived in a docstring for weeks while
|
| 878 |
+
being false. Nothing checked it. So the property is now enforced by a guard and the guard is
|
| 879 |
+
itself under test. Do not "simplify" either away.
|
| 880 |
+
|
| 881 |
+
- **Socket guard** (`tests/conftest.py`, module scope) — wraps `socket.socket.connect` /
|
| 882 |
+
`connect_ex` / `socket.create_connection` for the whole session and raises on any non-loopback
|
| 883 |
+
address, naming the host and pointing at conftest for the fix. Installed at *import* of
|
| 884 |
+
conftest, not as an autouse fixture, deliberately: the leaks that mattered happened at
|
| 885 |
+
**collection** time (a test module's `import gradio_ui` runs the app graph at module scope), and
|
| 886 |
+
a fixture runs too late to catch those. Loopback and AF_UNIX are allowed — local sockets are not
|
| 887 |
+
what the guard is about.
|
| 888 |
+
- **The three UI modules no longer touch the network.** `test_ui_handler_gating.py`,
|
| 889 |
+
`test_deploy_surface.py`, `test_machine_api_auth.py` all `import gradio_ui`, whose `gr.Blocks`
|
| 890 |
+
graph is built at module scope. Off a Space, gradio's `attach_oauth` falls back to
|
| 891 |
+
`_add_mocked_oauth_routes`, which calls HF `whoami` to mock a local login profile. **That is
|
| 892 |
+
gradio's debug convenience, not our code**, and it does not happen on the deployed Space (there
|
| 893 |
+
`SPACE_ID` is set and the real OAuth routes are used). conftest now stubs
|
| 894 |
+
`gradio.oauth._get_mocked_oauth_info`.
|
| 895 |
+
- **Why that seam and not the others considered:** it is the only function on that path that
|
| 896 |
+
touches the network, so stubbing it leaves every route gradio registers intact and the UI
|
| 897 |
+
tests still exercise the real graph. Restructuring `gradio_ui.py` to build Blocks inside a
|
| 898 |
+
function was rejected as far more invasive for a test-only problem — and its module-level
|
| 899 |
+
`demo` is what HF Spaces launches, so it must keep working. **`gradio_ui.py` is unchanged;
|
| 900 |
+
the entire fix is test-only.**
|
| 901 |
+
- **Known coupling:** `_get_mocked_oauth_info` is a gradio private. A gradio upgrade that
|
| 902 |
+
renames it would break the stub — which is why `test_hermetic.py` imports all three UI
|
| 903 |
+
modules explicitly, so that breakage surfaces as a named test failure rather than as a
|
| 904 |
+
mystery connection at collection time.
|
| 905 |
+
- **`tests/test_hermetic.py`** (new, 7 tests) — asserts the guard is installed, that it refuses a
|
| 906 |
+
routable address, that the message names the host *and* points at the fix (a guard with an
|
| 907 |
+
unhelpful message gets disabled by the next person who trips it), that loopback still works, and
|
| 908 |
+
that the three UI modules import cleanly.
|
| 909 |
+
- **Bonus: CI no longer needs an HF login.** `_get_mocked_oauth_info` raises `ValueError` when the
|
| 910 |
+
machine has no HF token, so those three modules could not have been imported on a clean CI box
|
| 911 |
+
at all. Verified with `HF_TOKEN` unset and `HF_HOME` pointed at an empty dir.
|
| 912 |
+
|
| 913 |
+
**Verified — 143 passed with sockets blocked outright and no `--ignore`** (136 before + 7 new),
|
| 914 |
+
using the same method that exposed the original bug: stub `socket.socket.connect` /
|
| 915 |
+
`socket.create_connection` to raise, then `pytest.main(["-q"])`. Previously 111 passed that way
|
| 916 |
+
with the three UI modules ignored.
|
| 917 |
+
|
| 918 |
+
**Mutation-tested, so the guard is not vacuous:** removing the `study_metadata` patch and the
|
| 919 |
+
`_req` stub reproduces the original bug, and the guard catches it with
|
| 920 |
+
`AssertionError: the test suite attempted a network connection to ('www.cbioportal.org', 443)`.
|
| 921 |
+
Restored after.
|
| 922 |
+
|
| 923 |
+
**Suite runtime 22s → 4s** — that difference *was* the network, on a good day. On a bad one it was
|
| 924 |
+
the 54-second, 12-error run that started this.
|
| 925 |
+
|
| 926 |
+
**No ADR.** The contested part (stub a gradio private vs. restructure `gradio_ui`) is test-only,
|
| 927 |
+
reversible, and documented at both the seam and the test. If it should be one, **next free is
|
| 928 |
+
0008** — `main` has 0006 (variant×subtype join) *and* 0007 (re-curation cadence) published — and
|
| 929 |
+
per `RESERVED.md` the claim must land on `main` alone, first.
|
| 930 |
+
|
| 931 |
+
⚠️ **This branch's `RESERVED.md` is stale: it stops at 0005 and states "next free 0006".** Both
|
| 932 |
+
0006 and 0007 are burned. This is the third instance of the same failure the ledger's own
|
| 933 |
+
postmortem describes — per-lane copies mean the one-shared-row conflict cannot fire between
|
| 934 |
+
branches, so *the ledger in your working tree is exactly the thing you cannot trust for this*.
|
| 935 |
+
Verify with `git show main:docs/adr/RESERVED.md` before claiming a number. (The rollup suggests
|
| 936 |
+
`origin/main`, but `origin` here IS the HF Space; local `main` is the reliable read.) Found by
|
| 937 |
+
reading `SHOWCASE_STATUS.md` before updating it — the same habit that caught collision #3.
|
| 938 |
+
|
| 939 |
+
**Environment note:** this repo now has a venv at `../../.venv` (Python 3.14) created for this
|
| 940 |
+
work — `gradio[oauth]==6.18.0` (the `oauth` extra is required or the UI modules fail to import at
|
| 941 |
+
all), plus the rest of `requirements.txt` and pytest. Previously the memory said to borrow
|
| 942 |
+
`DecoupleRpy_Agent/.venv`.
|
| 943 |
+
>>>>>>> dbaeaa3 (tests: enforce hermeticity with a socket guard; last 3 network-touching modules closed)
|
| 944 |
+
|
| 945 |
+
---
|
| 946 |
+
|
| 947 |
+
## 2026-07-28 — Routing leg VERIFIED end-to-end; dispatch blocked by a private-Space 404
|
| 948 |
+
|
| 949 |
+
The one thing the orchestrator registration task could not check headlessly — does the router
|
| 950 |
+
actually *classify* a DNA question and dispatch here — is now verified in a signed-in browser
|
| 951 |
+
session against the orchestrator **dev** Space (`5cbf1d9`).
|
| 952 |
+
|
| 953 |
+
Asked: *"Which samples in the TCGA pancreatic cohort carry a KRAS G12D mutation, and what is the
|
| 954 |
+
KRAS alteration frequency?"*
|
| 955 |
+
|
| 956 |
+
**Classification: PASS.** The orchestrator answered *"Routing to PDAC Genomics Agent for
|
| 957 |
+
computation."* — it recognised a DNA question and picked this specialist over the subtype and
|
| 958 |
+
DE agents. That is the registration working, including the deliberate choice to drop bare `KRAS`
|
| 959 |
+
from the trigger keywords (the hotspot allele `G12D` carried the intent instead).
|
| 960 |
+
|
| 961 |
+
**Dispatch: FAIL, and the error names its own cause.** The call came back:
|
| 962 |
+
|
| 963 |
+
> The specialist tool server could not be reached… `404 Client Error. Repository Not Found for
|
| 964 |
+
> url: https://huggingface.co/api/spaces/anne-voigt/pdac-genomics-agent`. If you are trying to
|
| 965 |
+
> access a private or gated repo, make sure you are authenticated…
|
| 966 |
+
|
| 967 |
+
This **Space is PRIVATE**, so without credentials the orchestrator cannot even *resolve* the repo,
|
| 968 |
+
let alone reach the gate. The failure is a repo-lookup 404, not an allow-list denial — consistent
|
| 969 |
+
with the caller-side root cause already found: the orchestrator attaches its token only when
|
| 970 |
+
`HF_TOKEN` is set, and that secret is unset on its Space.
|
| 971 |
+
|
| 972 |
+
**Nothing to change here.** The fix is one secret on the *orchestrator's* Space, and it is a
|
| 973 |
+
credential operation for a human. Worth noting the order of failure for whoever does it: with
|
| 974 |
+
`HF_TOKEN` unset the symptom is a 404 (invisible private repo); once set, the next thing exercised
|
| 975 |
+
is the allow-list — and the identity it resolves to must be on `ALLOWED_IDS` here
|
| 976 |
+
(`anne-voigt,cpelz741` today).
|
tests/conftest.py
CHANGED
|
@@ -2,8 +2,16 @@
|
|
| 2 |
|
| 3 |
Mirrors the sibling specialists: capture a few genes×samples of real API JSON once, commit
|
| 4 |
it, and route the client's fetch functions through it so CI is hermetic.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
import json
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
import pytest
|
|
@@ -11,6 +19,107 @@ import pytest
|
|
| 11 |
_FIXTURE = Path(__file__).parent / "fixtures" / "cbioportal_paad_mini.json"
|
| 12 |
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
@pytest.fixture
|
| 15 |
def mini():
|
| 16 |
return json.loads(_FIXTURE.read_text())
|
|
|
|
| 2 |
|
| 3 |
Mirrors the sibling specialists: capture a few genes×samples of real API JSON once, commit
|
| 4 |
it, and route the client's fetch functions through it so CI is hermetic.
|
| 5 |
+
|
| 6 |
+
"Hermetic" is **enforced**, not asserted: this module blocks outbound sockets for the whole
|
| 7 |
+
session (see below). It is enforced because the docstring alone was wrong for weeks — the
|
| 8 |
+
fixture curated, curation fetched `study_metadata`, and nothing patched it, so every
|
| 9 |
+
fixture-using test hit cbioportal.org. It surfaced as a run that failed with 12 errors and
|
| 10 |
+
passed on retry: a third party's latency reported as our bug. A property no test checks is a
|
| 11 |
+
property you do not have.
|
| 12 |
"""
|
| 13 |
import json
|
| 14 |
+
import socket
|
| 15 |
from pathlib import Path
|
| 16 |
|
| 17 |
import pytest
|
|
|
|
| 19 |
_FIXTURE = Path(__file__).parent / "fixtures" / "cbioportal_paad_mini.json"
|
| 20 |
|
| 21 |
|
| 22 |
+
# --------------------------------------------------------------------------- #
|
| 23 |
+
# Hermetic enforcement — no outbound sockets for the duration of the session
|
| 24 |
+
# --------------------------------------------------------------------------- #
|
| 25 |
+
# Installed at import of this module rather than as an autouse fixture, because the leaks we
|
| 26 |
+
# need to catch happen at *collection* time (a test module's `import gradio_ui` runs the whole
|
| 27 |
+
# app graph at module scope). conftest is imported before collection; a fixture would be too
|
| 28 |
+
# late.
|
| 29 |
+
|
| 30 |
+
_LOOPBACK = frozenset({"127.0.0.1", "::1", "localhost", "", "0.0.0.0"})
|
| 31 |
+
|
| 32 |
+
_real_connect = socket.socket.connect
|
| 33 |
+
_real_connect_ex = socket.socket.connect_ex
|
| 34 |
+
_real_create_connection = socket.create_connection
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _is_local(address) -> bool:
|
| 38 |
+
"""Loopback and AF_UNIX are allowed; everything routable is not.
|
| 39 |
+
|
| 40 |
+
Local sockets are how a test server or an ASGI transport talks to itself — legitimate, and
|
| 41 |
+
not what this guard is about. The guard is about reaching a *third party*.
|
| 42 |
+
"""
|
| 43 |
+
if isinstance(address, (str, bytes)): # AF_UNIX path
|
| 44 |
+
return True
|
| 45 |
+
if isinstance(address, tuple) and address:
|
| 46 |
+
host = address[0]
|
| 47 |
+
if isinstance(host, bytes):
|
| 48 |
+
host = host.decode("utf-8", "replace")
|
| 49 |
+
return str(host) in _LOOPBACK
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _blocked(address):
|
| 54 |
+
raise AssertionError(
|
| 55 |
+
f"the test suite attempted a network connection to {address!r}.\n"
|
| 56 |
+
"This suite is hermetic by contract: fixtures are pinned JSON captures and the request "
|
| 57 |
+
"path reads committed curated artifacts (ADR-0005 C4). A real connection here means "
|
| 58 |
+
"something is unpatched — patch it in tests/conftest.py rather than relaxing this "
|
| 59 |
+
"guard. Loopback is allowed if you genuinely need a local server."
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _guarded_connect(self, address):
|
| 64 |
+
if not _is_local(address):
|
| 65 |
+
_blocked(address)
|
| 66 |
+
return _real_connect(self, address)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _guarded_connect_ex(self, address):
|
| 70 |
+
if not _is_local(address):
|
| 71 |
+
_blocked(address)
|
| 72 |
+
return _real_connect_ex(self, address)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _guarded_create_connection(address, *args, **kwargs):
|
| 76 |
+
if not _is_local(address):
|
| 77 |
+
_blocked(address)
|
| 78 |
+
return _real_create_connection(address, *args, **kwargs)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
socket.socket.connect = _guarded_connect
|
| 82 |
+
socket.socket.connect_ex = _guarded_connect_ex
|
| 83 |
+
socket.create_connection = _guarded_create_connection
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# --------------------------------------------------------------------------- #
|
| 87 |
+
# Gradio's mocked-OAuth path — the one legitimate import-time connection, removed
|
| 88 |
+
# --------------------------------------------------------------------------- #
|
| 89 |
+
# `gradio_ui` builds its `gr.Blocks` graph at module scope (that module-level `demo` is what HF
|
| 90 |
+
# Spaces launches, so it stays that way). Off a Space, gradio's `attach_oauth` falls back to
|
| 91 |
+
# `_add_mocked_oauth_routes`, which calls huggingface_hub `whoami` to mock a local profile.
|
| 92 |
+
# That is gradio debug convenience, not our code, and it does not happen on the deployed Space
|
| 93 |
+
# (there `SPACE_ID` is set and the real OAuth routes are used instead). It also requires the
|
| 94 |
+
# developer's machine to be HF-logged-in, which CI is not.
|
| 95 |
+
#
|
| 96 |
+
# So we stub the mock. Patching `_get_mocked_oauth_info` — rather than `whoami` — is the
|
| 97 |
+
# narrowest seam: it is the only function on that path that touches the network, and replacing
|
| 98 |
+
# it leaves every route gradio registers intact, so the UI tests still exercise the real graph.
|
| 99 |
+
# Nothing here changes the deployed app; conftest is test-only.
|
| 100 |
+
def _install_offline_oauth_mock() -> None:
|
| 101 |
+
import gradio.oauth
|
| 102 |
+
|
| 103 |
+
gradio.oauth._get_mocked_oauth_info = lambda: {
|
| 104 |
+
"access_token": "test-token",
|
| 105 |
+
"token_type": "bearer",
|
| 106 |
+
"expires_at": 0,
|
| 107 |
+
"expires_in": 28800,
|
| 108 |
+
"scope": "openid profile",
|
| 109 |
+
"userinfo": {
|
| 110 |
+
"sub": "test-user-id",
|
| 111 |
+
"name": "Test User",
|
| 112 |
+
"preferred_username": "test-user",
|
| 113 |
+
"profile": "https://huggingface.co/test-user",
|
| 114 |
+
"picture": None,
|
| 115 |
+
"email": None,
|
| 116 |
+
},
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
_install_offline_oauth_mock()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
@pytest.fixture
|
| 124 |
def mini():
|
| 125 |
return json.loads(_FIXTURE.read_text())
|
tests/test_hermetic.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The suite is hermetic, and the guard that makes it so is itself under test.
|
| 2 |
+
|
| 3 |
+
`tests/conftest.py` claimed "NO live network" in a docstring for weeks while every
|
| 4 |
+
fixture-using test called cbioportal.org, because curation fetched `study_metadata` and
|
| 5 |
+
nothing patched it. The visible symptom was a run that failed with 12 errors and passed on
|
| 6 |
+
retry — a third party's latency arriving as our bug, at whatever moment they were slow.
|
| 7 |
+
|
| 8 |
+
The fix for *that* hole was to patch the function. The fix for the *class* of hole is this
|
| 9 |
+
file: the socket guard is enforcement, and enforcement nobody tests decays into the same
|
| 10 |
+
docstring. So these tests assert the guard is installed, that it actually refuses, and that
|
| 11 |
+
it names the host it refused — because a guard that fires with an unhelpful message gets
|
| 12 |
+
disabled by the next person who trips it.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import socket
|
| 18 |
+
|
| 19 |
+
import pytest
|
| 20 |
+
|
| 21 |
+
import conftest
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_socket_guard_is_installed():
|
| 25 |
+
"""If someone removes the guard, this is the test that notices."""
|
| 26 |
+
assert socket.socket.connect is conftest._guarded_connect
|
| 27 |
+
assert socket.create_connection is conftest._guarded_create_connection
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_outbound_connection_is_refused():
|
| 31 |
+
"""The guard must refuse a routable address — no DNS, no packets, no third party."""
|
| 32 |
+
with pytest.raises(AssertionError) as exc:
|
| 33 |
+
socket.create_connection(("www.cbioportal.org", 443), timeout=1)
|
| 34 |
+
assert "cbioportal" in str(exc.value)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_refusal_names_the_host_and_says_what_to_do():
|
| 38 |
+
"""A guard is only as good as the message it fails with."""
|
| 39 |
+
with pytest.raises(AssertionError) as exc:
|
| 40 |
+
socket.socket().connect(("huggingface.co", 443))
|
| 41 |
+
message = str(exc.value)
|
| 42 |
+
assert "huggingface.co" in message
|
| 43 |
+
assert "conftest" in message # points at the fix, not just the symptom
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_loopback_is_allowed():
|
| 47 |
+
"""Local sockets are not what this guard is about; blocking them would break test servers.
|
| 48 |
+
|
| 49 |
+
Asserted via `connect_ex` to a port nothing is listening on: a refused connection proves
|
| 50 |
+
the guard let the call through to the OS, which is the property under test.
|
| 51 |
+
"""
|
| 52 |
+
with socket.socket() as s:
|
| 53 |
+
s.settimeout(1)
|
| 54 |
+
assert s.connect_ex(("127.0.0.1", 1)) != 0 # reached the OS and was refused there
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@pytest.mark.parametrize(
|
| 58 |
+
"module", ["test_ui_handler_gating", "test_deploy_surface", "test_machine_api_auth"]
|
| 59 |
+
)
|
| 60 |
+
def test_ui_modules_import_without_network(module):
|
| 61 |
+
"""These three import `gradio_ui`, which builds its Blocks graph at module scope.
|
| 62 |
+
|
| 63 |
+
Off a Space, gradio's `attach_oauth` mocks the login button by calling HF `whoami` — a
|
| 64 |
+
real request, at import, before any test runs. conftest stubs that mock out. If the stub
|
| 65 |
+
stops matching gradio's internals (an upgrade renames `_get_mocked_oauth_info`), these
|
| 66 |
+
modules start hitting the network at *collection* time, which is exactly the failure this
|
| 67 |
+
suite is supposed to have stopped having. Importing them here proves the seam still holds.
|
| 68 |
+
"""
|
| 69 |
+
__import__(module)
|