pdac-genomics-agent-dev / tests /test_deploy_surface.py
avoigt1121
feat(panel): widen 19 → 29 genes (HR + MMR), and stop silently dropping uncurated genes
7ae3398
Raw
History Blame Contribute Delete
4.73 kB
"""The deploy surface exists and matches the published contract.
`deploy/orchestrator_registration.yaml` is what the orchestrator merges into its `agents.yaml`.
If a tool is renamed here and not there (or vice versa), the orchestrator dispatches into
nothing — so the descriptor and the live `gr.api` names are checked against each other.
"""
from __future__ import annotations
from pathlib import Path
import gradio_ui
def _descriptor() -> str:
return Path("deploy/orchestrator_registration.yaml").read_text()
def test_mcp_server_registers_the_contract_tool_names():
"""The MCP tool names ARE the published contract — not the Python wrapper names."""
import asyncio
from fastmcp import Client
from src.server import mcp
async def _names():
async with Client(mcp) as client:
return sorted(t.name for t in await client.list_tools())
assert mcp.name == "pdac-genomics-agent"
assert asyncio.run(_names()) == ["query_variant_status", "variant_by_subtype"]
def test_machine_endpoints_exist_with_request_injection():
"""Each `gr.api` target must accept an injected `request` — the token gate needs it."""
import inspect
for name in ("query_variant_status", "variant_by_subtype", "panel"):
fn = getattr(gradio_ui, name)
params = inspect.signature(fn).parameters
assert "request" in params, f"{name} cannot gate without an injected request"
assert params["request"].default is None
def test_api_names_match_the_orchestrator_descriptor():
"""The frozen names in the descriptor must be the ones actually registered."""
descriptor = _descriptor()
for tool_name in ("query_variant_status", "variant_by_subtype"):
assert tool_name in descriptor
def test_descriptor_arg_names_match_the_live_signatures():
"""The published `args` must be the parameters the endpoints actually take.
String equality on tool names caught renames but not the drift that actually happened:
the descriptor advertised two args for `variant_by_subtype` (which takes four) and
`list[str]` for `genes` (which is a comma-separated string). Compare structurally.
"""
import inspect
import yaml # a gradio dependency, so always present where the Space runs
tools = {t["name"]: t for t in yaml.safe_load(_descriptor())["tools"]}
for name, entry in tools.items():
params = inspect.signature(getattr(gradio_ui, name)).parameters
declared = set(entry["args"] or {})
live = {p for p in params if p != "request"}
assert declared == live, f"{name}: descriptor says {declared}, live takes {live}"
for arg in declared:
# PEP 563 is on in `gradio_ui`, so annotations arrive as strings.
assert params[arg].annotation == "str", f"{name}.{arg} is not a plain string"
assert entry["args"][arg] == "str", (
f"{name}.{arg} is declared as {entry['args'][arg]!r}, but every machine "
"argument is a plain string"
)
def test_descriptor_does_not_restate_the_cohort_set_wrongly():
"""Whatever cohorts the descriptor names must be curated artifacts that exist."""
import re
import yaml
doc = yaml.safe_load(_descriptor())
curated = {p.stem for p in Path("src/resources/curated").glob("*.json")}
# Study ids appear in the illustrative tail of `cohorts:` as a comma-separated run.
named = set(re.findall(r"(?:^|[ ,])([a-z][a-z0-9]+(?:_[a-z0-9]+)+)(?=[,.\s]|$)", doc["cohorts"]))
named -= {"src", "resources", "curated", "json"}
assert named == curated, (
f"descriptor cohort list has drifted — extra: {sorted(named - curated)}, "
f"missing: {sorted(curated - named)}"
)
def test_blocks_app_is_importable():
"""The Space's `app_file` must expose a `demo` that built without error."""
assert gradio_ui.demo is not None
assert gradio_ui.demo.title == "PDAC Genomics Agent"
def test_panel_endpoint_returns_the_v1_panel(monkeypatch):
"""The orchestrator can ask for the panel rather than hardcoding the gene list.
Asserted against `load_panel()` rather than a literal count, precisely because the panel is
config that widens (19 → 29 on 2026-08-05). A hardcoded number here would fail on every
widening for no reason; what actually matters is that the endpoint and the resource agree.
"""
import json
from src.workflows.variant_status import load_panel
monkeypatch.delenv("ACCESS_CONTROL", raising=False)
body = json.loads(gradio_ui.panel(request=None))
assert body["n_genes"] == len(load_panel())
assert body["panel"] == load_panel()
assert "KRAS" in body["panel"]