Skip to content

Commit 24fbcbf

Browse files
lucapinelloclaude
andauthored
The outage skip I added this morning was too generous in three ways (#210)
An adversarial review found three paths by which a genuine report defect could take the skip I added in #206. All three reproduce by reading the code, and all three are mine. **Uncaught JS was swallowed.** `unreachable_external_host` looked only at `console_errors`, and the callers skip *before* asserting `page_errors`. So a report throwing an uncaught exception was skipped whenever its console errors happened to be outage-shaped — the single worst outcome for a guard whose entire job is deciding whether CI may fail. A remote host refusing a connection cannot cause an uncaught exception in page code, so one is now disqualifying on its own. The assertion also moved *before* the skip: redundant now, but the ordering was the root cause and is what a future edit is most likely to get wrong again. **Any failing external URL counted.** The old version required only that *some* external request had been attempted — and since all 19 reports contact UCSC, that was satisfied essentially always. One unrelated refused request therefore disabled every verdict for that report: blank-canvas, `_MIN_CANVASES`, convergence and the load budget. The URLs implicated in the failures must now all resolve to a reference-sequence host. **A wrong host committed inside a report looked like weather.** A typo'd or third-party host baked into a report is a permanent defect for every reader, and it produced the same console error as an outage — so it would have been skipped forever. Restricting the skip to `REFERENCE_SEQUENCE_HOSTS` makes a report citing anything else fail. My first attempt at that last fix was itself wrong, and this file's own new test caught it: I compared with `host in url`, which also matches `hgdownload.soe.ucsc.edu.example.net` — letting a hijacked domain inherit the trusted host's exemption. Now an exact netloc comparison via `urlparse`. One case stays genuinely undecidable and is documented rather than papered over: a *correct* reference URL that is permanently dead is indistinguishable from a transient outage in a single render. It skips, and the docstring says where to look if UCSC ever retires that path. 5 new tests, one per hole plus the message accuracy and the case the skip exists for. Browser suite 46 passed with UCSC reachable; fast suite 2,013 passed / 30 skipped. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c6c86ef commit 24fbcbf

3 files changed

Lines changed: 133 additions & 13 deletions

File tree

tests/browser_harness.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,33 +142,83 @@ def summary(self) -> str:
142142
)
143143

144144

145+
#: The only host a committed report is *expected* to fetch from: igv.js needs a sequence source and
146+
#: hg38 is ~3 GB, so the reference two-bit is resolved from UCSC at open time. Anything else failing
147+
#: is a defect in the report, not weather — so it must not qualify for a skip.
148+
#:
149+
#: Compared as an **exact netloc**, never as a substring. `"hgdownload.soe.ucsc.edu" in url` also
150+
#: matches `hgdownload.soe.ucsc.edu.example.net`, which would let a typo'd or hijacked domain inherit
151+
#: the trusted host's exemption. That was the first implementation, and this file's own test caught it.
152+
REFERENCE_SEQUENCE_HOSTS = frozenset({"hgdownload.soe.ucsc.edu"})
153+
154+
155+
def _urls_in(message: str) -> list:
156+
"""URLs named inside a console error, e.g. igv.js's "Error accessing resource: <url> Status: 0"."""
157+
import re
158+
159+
return re.findall(r"https?://[^\s\"'<>]+", message)
160+
161+
145162
def unreachable_external_host(r) -> "str | None":
146163
"""Why this render cannot be judged, or None if it can.
147164
148165
Every committed report resolves its reference sequence from `hgdownload.soe.ucsc.edu`: igv.js
149166
requires a sequence source, hg38 is ~3 GB, and bundling it in a repo is not an option — a
150-
documented limitation rather than a defect. The consequence is that this file's verdict depended
151-
on a third party being up. On 2026-08-14 UCSC refused the connection during one PR's run and the
167+
documented limitation rather than a defect. The consequence was that this file's verdict depended
168+
on a third party being up: on 2026-08-14 UCSC refused the connection during one PR's run and the
152169
same corpus that had passed minutes earlier reported `canvases 0/0 painted (NOT converged)` and
153170
blew the 30 s budget at the 60 s timeout — four failures, none of them about the reports.
154171
155-
A test that goes red when someone else's server hiccups trains people to re-run CI until it is
156-
green, which is how a real blank-panel regression gets waved through. So: if the *only* console
157-
errors are unreachable-host signatures and an external host was actually contacted, this render
158-
is unverifiable and says so. A genuinely broken report still fails — a blank canvas with UCSC up
159-
produces no such console error, and any error outside this list disqualifies the skip.
172+
A suite that goes red when someone else's server hiccups trains people to re-run CI until it is
173+
green, which is how a real blank-panel regression gets waved through. But the first version of
174+
this function was far too generous, and an adversarial review found three ways a genuine defect
175+
could take the skip. All three are now closed:
176+
177+
* **Uncaught JS was swallowed.** The old version looked only at `console_errors`, and the callers
178+
skip *before* asserting `page_errors` — so a report throwing an uncaught exception was skipped
179+
whenever the console errors happened to be outage-shaped. An uncaught error is now
180+
disqualifying on its own: it cannot be caused by a remote host refusing a connection.
181+
* **Any failing external URL counted.** The old version required only that *some* external
182+
request had been attempted. Since all 19 reports contact UCSC, that was satisfied
183+
essentially always, so one unrelated refused request disabled every verdict for that report.
184+
Now the URLs named in the failures must all be reference-sequence URLs.
185+
* **A wrong URL committed inside a report looked like weather.** A typo'd or third-party host
186+
baked into a report is a permanent defect for every reader, and it produced the same console
187+
error as an outage. Restricting the skip to `REFERENCE_SEQUENCE_HOSTS` means a report citing
188+
anything else fails instead of being skipped forever.
189+
190+
One case remains genuinely undecidable and is called out rather than papered over: a *correct*
191+
reference URL that is permanently dead looks exactly like a transient outage. Nothing observable
192+
in one render distinguishes them, so it skips. If UCSC ever retires that path, every report will
193+
skip rather than fail, and this docstring is the note that says to look here.
160194
"""
161195
if not r.console_errors or not r.external_urls:
162196
return None
197+
198+
# An uncaught JS exception is never explained by an unreachable host.
199+
if getattr(r, "page_errors", None):
200+
return None
201+
163202
unexplained = [e for e in r.console_errors
164203
if not any(sig in e for sig in UNREACHABLE_SIGNATURES)]
165204
if unexplained:
166205
return None
167-
hosts = ", ".join(r.external_hosts) or "an external host"
206+
207+
# Every URL implicated in the failures must be one we expect a report to fetch. When a message
208+
# names no URL (Chromium's bare "Failed to load resource: net::ERR_*"), fall back to requiring
209+
# that every external request this report made was a reference-sequence one.
210+
named = [u for e in r.console_errors for u in _urls_in(e)]
211+
implicated = named or list(r.external_urls)
212+
hosts = {urlparse(u).netloc for u in implicated}
213+
foreign = sorted(h for h in hosts if h not in REFERENCE_SEQUENCE_HOSTS)
214+
if foreign:
215+
return None
216+
217+
where = ", ".join(sorted(hosts)) or "the reference host"
168218
return (
169-
f"{r.path.name}: {hosts} did not answer, so the reference sequence never loaded and the "
170-
f"panel could not paint. Not a report defect -- igv.js needs a sequence source and hg38 is "
171-
f"too large to bundle. Errors: {r.console_errors[:2]}"
219+
f"{r.path.name}: {where} did not answer, so the reference sequence could not load. Not a "
220+
f"report defect -- igv.js needs a sequence source and hg38 is too large to bundle. "
221+
f"Errors: {r.console_errors[:2]}"
172222
)
173223

174224

tests/test_browser_outage_guard.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,69 @@ def test_errors_without_any_external_request_are_never_an_outage():
6868

6969
def test_a_clean_render_needs_no_excuse():
7070
assert bh.unreachable_external_host(_fake()) is None
71+
72+
73+
# ── the three holes an adversarial review found in the first version ─────────────
74+
75+
def test_an_uncaught_js_exception_is_never_excused_by_an_outage():
76+
"""The worst of the three: callers skip BEFORE asserting page_errors.
77+
78+
A report throwing an uncaught exception was skipped whenever its console errors happened to be
79+
outage-shaped — so a genuine JS fault in a committed report could hide behind UCSC being down.
80+
A remote host refusing a connection cannot cause an uncaught exception in page code.
81+
"""
82+
r = _fake(console=_REAL_OUTAGE, external=_UCSC,
83+
page=["TypeError: Cannot read properties of undefined (reading 'tracks')"])
84+
assert bh.unreachable_external_host(r) is None, (
85+
"a render with uncaught JS was treated as an outage; the paint test skips before it asserts "
86+
"page_errors, so that fault would never be reported"
87+
)
88+
89+
90+
def test_a_failing_third_party_url_is_not_an_outage_excuse():
91+
"""The skip once required only that *some* external request had been attempted.
92+
93+
All 19 reports contact UCSC, so that condition was satisfied essentially always — meaning one
94+
unrelated refused request disabled every verdict for the report, including blank-canvas.
95+
"""
96+
r = _fake(
97+
console=["error: IGV error: Error accessing resource: "
98+
"https://cdn.example.com/tracks/mystery.bw Status: 0"],
99+
external=["https://cdn.example.com/tracks/mystery.bw"] + _UCSC,
100+
)
101+
assert bh.unreachable_external_host(r) is None, (
102+
"a report failing to fetch a non-reference third-party URL was skipped. That is a defect in "
103+
"the report — it should not ship a dependency on cdn.example.com — and must fail, not skip."
104+
)
105+
106+
107+
def test_a_typoed_reference_host_committed_in_a_report_still_fails():
108+
"""A wrong host baked into a report is permanent for every reader, not weather."""
109+
bad = "https://hgdownload.soe.ucsc.edu.example.net/goldenPath/hg38/bigZips/hg38.2bit"
110+
r = _fake(console=[f"error: IGV error: Error accessing resource: {bad} Status: 0"], external=[bad])
111+
why = bh.unreachable_external_host(r)
112+
assert why is None, (
113+
f"a report citing {bad} was skipped as an outage. A typo'd or hijacked host is a permanent "
114+
f"defect for every reader and must fail. (got: {why})"
115+
)
116+
117+
118+
def test_the_real_reference_url_still_skips():
119+
"""The whole point: the genuine UCSC outage must still be tolerated."""
120+
r = _fake(console=_REAL_OUTAGE, external=_UCSC)
121+
why = bh.unreachable_external_host(r)
122+
assert why and "hgdownload.soe.ucsc.edu" in why, (
123+
f"tightening the guard broke the case it exists for: {why!r}"
124+
)
125+
126+
127+
def test_the_skip_message_names_only_hosts_that_actually_failed():
128+
"""The old message listed every contacted host and asserted the sequence never loaded.
129+
130+
With the URL now extracted from the error itself, the message can only name what failed.
131+
"""
132+
r = _fake(console=_REAL_OUTAGE, external=_UCSC + ["https://fonts.googleapis.com/css"])
133+
why = bh.unreachable_external_host(r) or ""
134+
assert "fonts.googleapis.com" not in why, (
135+
f"the skip message blames a host that did not fail: {why!r}"
136+
)

tests/test_committed_reports_render_in_a_browser.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,15 @@ def test_every_committed_report_paints_every_track(browser, report):
129129
"""
130130
r = _render_once(browser, report)
131131

132+
# Asserted BEFORE the outage skip, deliberately. `unreachable_external_host` also refuses to
133+
# excuse a render with uncaught JS, so this is belt and braces — but the original defect was
134+
# precisely that the skip ran first and swallowed this assertion, and the ordering is the part a
135+
# future edit is most likely to get wrong again.
136+
assert not r.page_errors, f"{r.summary()}\nuncaught JS: {r.page_errors[:3]}"
137+
132138
unverifiable = bh.unreachable_external_host(r)
133139
if unverifiable:
134140
pytest.skip(unverifiable)
135-
136-
assert not r.page_errors, f"{r.summary()}\nuncaught JS: {r.page_errors[:3]}"
137141
assert not r.console_errors, f"{r.summary()}\nconsole errors: {r.console_errors[:3]}"
138142

139143
if not r.is_igv:

0 commit comments

Comments
 (0)