Spaces:
Sleeping
Sleeping
| """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 {} | |
| 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 ------------------------------- | |
| 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"]) | |