Spaces:
Sleeping
Sleeping
File size: 7,971 Bytes
9f3852c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | """The modality surface: the UI radio, the machine endpoint, and the refusal between them.
Two facts drive this file, and they pull in opposite directions.
**A closed-choice widget can never exercise the refusal.** `bs_modality` is a `gr.Radio`, so
gradio validates the submitted value against the radio's own `choices` and returns a generic
``Value: 'fusion' β¦ is not in the list of choices`` before `variant_by_subtype` is called. That
is true for ANY choice set β widening the radio to three options does not make the fourth value
reachable, it just moves the wall. So the `unsupported_modality` refusal cannot be owned by a
click-through, ever, and these tests own it instead. That is not a stopgap; it is where the case
belongs, because the only surface that can carry an unrecognized modality is the `gr.api`
machine endpoint the orchestrator calls with a free-form string.
**But the radio still has to match the tool.** For the whole life of the SV modality (ADR-0008)
the radio's literal was `["mutation", "cnv"]` while `variant_by_subtype` accepted `sv` and
`deploy/orchestrator_registration.yaml` published it β the UI offering a narrower contract than
the machine endpoint, which is the same family of failure as the modality fallback this refusal
was written to prevent: the surface answering a different question than the one on the label.
`test_ui_modality_choices_equal_the_tools_closed_set` is the regression that would have caught
it on the day `sv` landed, and will catch the fourth modality too.
"""
from __future__ import annotations
import json
import pytest
import gradio_ui
from src.tools.variant_by_subtype import SUPPORTED_MODALITIES, variant_by_subtype
class _Request:
"""Minimal stand-in for `gr.Request` β only `.headers.get` is used."""
def __init__(self, headers=None):
self.headers = headers or {}
@pytest.fixture(autouse=True)
def _open_gate(monkeypatch, tmp_path):
"""These tests are about the modality contract, not the auth gate (test_access owns that)."""
monkeypatch.delenv("ACCESS_CONTROL", raising=False)
monkeypatch.setenv("LOG_SINK", "local")
monkeypatch.setenv("LOG_SINK_LOCAL_DIR", str(tmp_path / "run_logs"))
def _modality_radio():
"""The Variant Γ subtype tab's modality radio, found in the built Blocks graph."""
radios = [
block
for block in gradio_ui.demo.blocks.values()
if block.__class__.__name__ == "Radio" and getattr(block, "label", None) == "Modality"
]
assert len(radios) == 1, f"expected exactly one Modality radio, found {len(radios)}"
return radios[0]
# --- the UI surface must equal the tool's closed set ----------------------------------
def test_ui_modality_choices_equal_the_tools_closed_set():
"""The radio is DERIVED from `SUPPORTED_MODALITIES`; this locks that it stays derived.
Fails if a modality is added to the tool and not the UI (the 2026-08-05 `sv` gap), and
fails if a value is added to the radio that the tool would refuse β which would turn an
ordinary user click into an `unsupported_modality` error.
"""
# gradio normalizes `choices` to (label, value) pairs.
offered = {c[1] if isinstance(c, (tuple, list)) else c for c in _modality_radio().choices}
assert offered == set(SUPPORTED_MODALITIES)
def test_sv_is_offered_and_mutation_stays_the_default():
radio = _modality_radio()
offered = {c[1] if isinstance(c, (tuple, list)) else c for c in radio.choices}
assert "sv" in offered, "ADR-0008's SV modality must be reachable from the UI"
assert radio.value == "mutation"
# --- the refusal, on the only surface that can reach it -------------------------------
@pytest.mark.parametrize("bad", ["fusion", "mutaton", "expression", "SV_STATUS"])
def test_machine_endpoint_refuses_an_unsupported_modality(patched_cbio, bad):
"""The `gr.api` endpoint is where a free-form modality string actually arrives.
The radio cannot produce one; the orchestrator can, and did β this is the path that used
to answer a fusion question with a mutation association and `join_available: true`.
"""
body = json.loads(
gradio_ui.variant_by_subtype(
genes="KRAS", source="cbioportal:paad_tcga", modality=bad, request=_Request()
)
)
assert body["join_available"] is False
assert body["route"] == "unsupported_modality"
assert set(body["supported_modalities"]) == set(SUPPORTED_MODALITIES)
assert "genes" not in body, "no association may be computed for a refused modality"
def test_machine_endpoint_keeps_blank_meaning_unspecified(patched_cbio):
"""Blank is the ordinary case, NOT an unknown value β an untouched field sends "".
Guarded here as well as at the tool, because the normalization that makes it work
(`(modality or "mutation").strip()`) lives in `gradio_ui`, and a refusal here would break
every orchestrator call that leaves the field alone.
"""
for blank in ("", " ", "\t"):
body = json.loads(
gradio_ui.variant_by_subtype(
genes="KRAS", source="cbioportal:paad_tcga", modality=blank, request=_Request()
)
)
assert body.get("route") != "unsupported_modality", f"blank {blank!r} means 'unspecified'"
assert body["modality"] == "mutation"
# --- a refusal must be VISIBLE, not just present in the JSON --------------------------
#
# Both routes below became clickable when `sv` joined the radio: 5 of the 7 curated cohorts
# publish no SV at all. Before this, neither had a caution branch β they fell through to
# `caveats`, which a refusal does not carry, so the box stayed `visible=False` and the reader
# saw an empty plot and nothing else. That is the ADR-0004 invisible-denial failure in a second
# place: an answer that refuses in the JSON and looks merely empty on screen.
def test_unavailable_modality_is_visible_in_the_caution_box(patched_cbio):
result = variant_by_subtype(["KRAS"], "cbioportal:paad_tcga", modality="sv")
assert result["route"] == "unavailable_modality" # TCGA publishes no SV
caution = gradio_ui._subtype_caution_md(result)
assert caution["visible"] is True
assert "no sv data" in caution["value"].lower()
def test_unsupported_modality_is_visible_in_the_caution_box(patched_cbio):
result = variant_by_subtype(["KRAS"], "cbioportal:paad_tcga", modality="fusion")
assert result["route"] == "unsupported_modality"
caution = gradio_ui._subtype_caution_md(result)
assert caution["visible"] is True
assert "unsupported modality" in caution["value"].lower()
for modality in SUPPORTED_MODALITIES:
assert modality in caution["value"]
# --- the SV join is a real capability, not a decorative choice -------------------------
def test_sv_join_returns_counts_and_the_sparsity_caveat():
# No `patched_cbio`: this reads the COMMITTED `ccle_broad_2019` artifact, the only curated
# cohort carrying both SV and a subtype-shaped clinical attribute. Still hermetic β since
# ADR-0005 C4 the request path reads artifacts and never calls the API.
"""Adding the choice is only defensible if `sv` can actually answer.
It can: on a cohort carrying both SV and a named subtype attribute, the join runs and
returns contingency counts. Most genes fall below `MIN_ALTERED_FOR_TEST` and report
`testable: false` β that is the honest form of a rare event, and the SV caveat says so, so
a reader cannot mistake sparse counts for an absence of association.
"""
result = variant_by_subtype(
["KRAS", "NRG1"],
"cbioportal:ccle_broad_2019",
subtype_attribute="SUBTYPE",
modality="sv",
)
assert result["join_available"] is True
assert result["modality"] == "sv"
assert result["genes"], "an SV join must return per-gene blocks, not an empty answer"
assert any("Structural variants are RARE" in c for c in result["caveats"])
|