pdac-genomics-agent-dev / tests /test_plot_and_caveat_rendering.py
avoigt1121
fix(ui): make the raw-result accordion legible without touching the payload
2f6f12d
Raw
History Blame Contribute Delete
11.1 kB
"""What the SCREEN says, not what the payload says — the two ways it disagreed.
Both bugs guarded here were found by clicking through the deployed Space on 2026-08-05, and
neither was visible to any existing test: every payload involved was correct. The tests below
therefore assert on the rendered surface (the plot's colour scale, the caution markdown), which
is the only place these failed.
1. The chart derived its colour scale from whatever modalities the CURRENT frame held, and the
frontend did not rebuild that domain when the next frame held more. A single-gene query
followed by a full-panel query drew an empty sv-only chart while the JSON reported KRAS at
93.7%. Pinning `color_map` makes the domain independent of the data.
2. `caveats` — which carries the ADR-0008 SV annotation-depth warning — was rendered only on the
subtype tab, never on the variant-status tab where SV data is actually queried.
"""
from __future__ import annotations
import gradio_ui
def _result(genes, *, n_profiled=None, caveats=None, **extra):
payload = {
"source": "cbioportal:test",
"grounded": True,
"genes": genes,
"n_profiled": n_profiled or {"mutation": 100, "cnv": 100, "sv": 100},
}
if caveats is not None:
payload["caveats"] = caveats
payload.update(extra)
return payload
def _freq(pct):
return {"frequency": pct / 100, "n_altered": int(pct), "n_profiled": 100}
class _Profile:
def __init__(self, username):
self.username = username
def _rows(plot):
"""`plot.value` is the serialised {columns, data} the browser receives, not a DataFrame."""
cols = plot.value["columns"]
return [dict(zip(cols, row)) for row in plot.value["data"]]
# --------------------------------------------------------------------------- #
# 1. the colour scale must not depend on the frame
# --------------------------------------------------------------------------- #
# The exact live sequence: NRG1 alone on `pdac_msk_2024` yields an SV-ONLY frame, because NRG1
# is off that cohort's DNA panel and carries no mutation/cnv block at all.
_SV_ONLY = _result({"NRG1": {"sv": _freq(0.3)}})
_FULL_PANEL = _result(
{
"KRAS": {"mutation": _freq(93.7), "cnv": _freq(1.9), "sv": _freq(0.0)},
"NRG1": {"sv": _freq(0.3)},
}
)
def test_one_chart_per_modality_never_a_stacked_one():
"""Stacking summed three modalities into one bar and put KRAS at ~120% on a "% altered" axis.
Two independent things were wrong with that: a sample can be BOTH mutated and copy-number
altered, so the parts are not disjoint and adding them double-counts; and the modalities have
different denominators, so the segments are not percentages of the same thing. A stacked bar
asserts "these parts make up the whole" and neither held.
"""
plots = gradio_ui._frequency_plots(_FULL_PANEL)
assert len(plots) == len(gradio_ui._MODALITY_ORDER) == 3
for plot in plots:
# Single series per chart: no `color` encoding at all, so nothing can stack.
assert getattr(plot, "color", None) is None
assert set(plot.value["columns"]) == {"gene", "frequency"}
def test_each_chart_names_its_own_denominator():
""""% altered" is meaningless without saying of what — and it differs per modality.
`ccle_broad_2019` profiles 53 samples for mutation, 44 for cnv and 41 for sv. Reading one
axis across three modalities was exactly the mistake the stacked chart invited.
"""
result = _result(
{"KRAS": {"mutation": _freq(84.0), "cnv": _freq(60.0), "sv": _freq(2.0)}},
n_profiled={"mutation": 53, "cnv": 44, "sv": 41},
)
titles = [p.title for p in gradio_ui._frequency_plots(result)]
assert "53 samples profiled" in titles[0]
assert "44 samples profiled" in titles[1]
assert "41 samples profiled" in titles[2]
def test_every_axis_starts_at_zero_and_stops_at_100():
"""A bar measured from a non-zero baseline misstates every ratio on the chart.
Autoscaled, gradio started a CCLE axis at 20%, which made SMAD4 at ~21% look like nothing
next to KRAS at 90%. And a "% altered" axis has no business running past 100.
"""
result = _result(
{"KRAS": {"mutation": _freq(90.6)}, "SMAD4": {"mutation": _freq(22.6)}},
n_profiled={"mutation": 53},
)
low, high = gradio_ui._frequency_plots(result)[0].y_lim
assert low == 0
assert 90.6 < high <= 100
def test_a_tiny_but_real_frequency_stays_visible():
"""NRG1 is 6/2336 = 0.26%. On a fixed 0-100 axis that is a sub-pixel sliver.
Rendering a real finding as nothing is the same failure as dropping it, so the ceiling is
fitted per chart. The floor stops one tiny bar being stretched to full height instead, which
would read as "most of the cohort".
"""
sv_only = _result({"NRG1": {"sv": _freq(0.26)}}, n_profiled={"sv": 2336})
_, _, sv = gradio_ui._frequency_plots(sv_only)
assert sv.y_lim[0] == 0
assert sv.y_lim[1] <= 1.0, "ceiling must fit the data, not the percentage range"
assert sv.y_lim[1] >= 0.26, "the bar must fit inside the axis"
def test_a_modality_the_answer_lacks_is_hidden_not_drawn_empty():
"""An empty axis reads as "we looked and found none" — the claim we most want not to make.
`paad_tcga` has no SV profile at all, so its SV chart must not exist on the page rather than
appear as a flat zero line.
"""
no_sv = _result(
{"KRAS": {"mutation": _freq(90.7), "cnv": _freq(2.0)}},
n_profiled={"mutation": 150, "cnv": 184},
unavailable_modalities=["sv"],
)
mutation, cnv, sv = gradio_ui._frequency_plots(no_sv)
assert mutation.visible is True and cnv.visible is True
assert sv == {"__type__": "update", "visible": False}
def test_charts_keep_a_fixed_position_between_answers():
"""Chart 1 is always mutation, chart 3 always SV — a reader should not have to check."""
wide = gradio_ui._frequency_plots(_FULL_PANEL)
narrow = gradio_ui._frequency_plots(_SV_ONLY)
assert "Mutated" in wide[0].title and "fusion" in wide[2].title.lower()
# The sv-only answer leaves the first two slots empty rather than promoting SV to slot 1.
assert narrow[0] == {"__type__": "update", "visible": False}
assert "fusion" in narrow[2].title.lower()
kras = [r for r in _rows(wide[0]) if r["gene"] == "KRAS"]
assert kras and kras[0]["frequency"] == 93.7
def test_an_unassayed_gene_is_still_never_given_a_zero():
"""Stabilising the SCALE must not stabilise the DATA by inventing wild-type rows.
The tempting fix — pad every gene to all three modalities so the category set never changes
— would report NRG1 as 0% mutated on a cohort that never sequenced it, which is the exact
confident false negative the `assayed: false` gate exists to prevent.
"""
frame = gradio_ui._frequency_frame(_FULL_PANEL)
nrg1 = frame[frame["gene"] == "NRG1"]
assert set(nrg1["modality"]) == {"sv"}
def test_variant_status_handler_clears_the_plot_before_answering():
"""The chart must be blanked between answers, not overwritten in place.
Shipped in 23d37b6 without this and caught on prod the same day: a query that NARROWED the
gene set left the previous answer's bars under the new answer's caution — one cohort's
numbers beneath another cohort's warning. Both a stale chart and the earlier dropped-marks
bug are the same failure, just in opposite directions, which is why the test asserts the
SHAPE of the handler (clear, then answer) rather than either symptom.
An empty chart claims nothing. A stale chart claims something false.
"""
import inspect
assert inspect.isgeneratorfunction(gradio_ui._ui_variant_status)
frames = list(
gradio_ui._ui_variant_status(
"cbioportal:paad_tcga", ["KRAS"], profile=_Profile("anne-voigt")
)
)
assert len(frames) == 2, "expected a clearing frame followed by the answer"
clearing, answer = frames
# The clearing frame blanks each result slot's VALUE and leaves visibility alone. Hiding here
# instead made every query a hide→show flip, and the third chart lost that race about half
# the time — it stayed unmounted ("null does not exist" in the browser), so a cohort WITH
# structural variants rendered as mutation+CNV only, reading as "no fusions here".
#
# Two spellings of "blanked", because the slots are no longer all charts: a chart clears to a
# bare `None`, while the summary table and the full-result download clear with an explicit
# `gr.update(value=None)`. The invariant is the same for all of them and is what this asserts
# — carry no value, and say nothing about visibility. The download slot matters most here: a
# stale file link would leave the page entirely and be opened later with nothing to say which
# query produced it.
for slot in clearing[1:-1]:
if slot is None:
continue
assert isinstance(slot, dict), f"unexpected clearing-frame slot: {slot!r}"
assert slot.get("value") is None, "clearing frame must carry no value"
assert "visible" not in slot, "clearing frame must blank, not hide"
assert any(getattr(u, "value", None) is not None for u in answer[1:-1])
# --------------------------------------------------------------------------- #
# 2. caveats must reach the screen on the tab that produces them
# --------------------------------------------------------------------------- #
_SV_CAVEAT = (
"This cohort's structural-variant calls carry NO frame annotation - every event is "
"reported as `rearrangement`, and this cohort can never return `fusion_in_frame`."
)
def test_variant_status_caution_renders_caveats():
"""The ADR-0008 warning has to be visible on the tab where SV data is queried."""
update = gradio_ui._status_caution_md(
_result({"KRAS": {"mutation": _freq(90.0)}}, caveats=[_SV_CAVEAT])
)
assert update["visible"] is True
assert "fusion_in_frame" in update["value"]
assert _SV_CAVEAT in update["value"]
def test_caveats_render_alongside_the_other_cautions_not_instead_of_them():
"""Licence and coverage warnings must survive the addition — they share one markdown box."""
update = gradio_ui._status_caution_md(
_result(
{"KRAS": {"mutation": _freq(90.0)}},
caveats=[_SV_CAVEAT],
unavailable_modalities=["expression"],
citation={
"study_terms": {
"commercial_use": "restricted",
"commercial_use_basis": {"instrument": "Broad DepMap/CCLE Portal Terms"},
}
},
)
)
text = update["value"]
assert "expression" in text
assert _SV_CAVEAT in text
assert "Licensing" in text
def test_no_caveats_leaves_the_box_untouched():
"""An answer with nothing to warn about must not grow an empty banner."""
update = gradio_ui._status_caution_md(_result({"KRAS": {"mutation": _freq(90.0)}}))
assert update["visible"] is False