File size: 5,599 Bytes
1f7de2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b57f66
 
 
 
 
 
1f7de2a
 
1b57f66
1f7de2a
1b57f66
 
 
1f7de2a
1b57f66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f7de2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01b8103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests for the fold_structure agent tool β€” the Bench's auto-opening 3-D view.

The privacy contract is the point of this tool and is asserted here: it must
resolve a NAMED protein to a PUBLIC AlphaFold-DB model by sending only the gene
symbol + organism, and must never accept or forward a sequence (de-novo folding
of a user's own variant goes to a third party, so it stays an explicit,
separately-consented client-side action).
"""
import pytest

from dee.core import agent_tools as t


def test_fold_structure_is_registered_and_specced():
    assert "fold_structure" in t._TOOLS
    spec = next(s for s in t.TOOL_SPECS if s["function"]["name"] == "fold_structure")
    params = spec["function"]["parameters"]
    assert set(params["required"]) == {"gene_symbol", "organism"}
    # It must NOT take a sequence β€” that's the whole privacy boundary.
    assert "sequence" not in params["properties"]
    # NO enum on organism. AlphaFold DB covers all of UniProt, and an enum is
    # a hard gate: the model physically cannot emit "yeast" if the schema only
    # lists human and mouse, so it reports the organism as unsupported instead
    # of trying. That is how SpCas9 β€” modelled as Q99ZW2 the whole time β€” came
    # back to a user as "no structure available".
    assert "enum" not in params["properties"]["organism"]


def test_fold_structure_requires_gene_and_organism():
    assert t._tool_fold_structure({"organism": "human"})["ok"] is False
    # A bare symbol IS genuinely ambiguous, so an empty organism is refused β€”
    # but the refusal must be "which organism?", never "that one isn't allowed".
    bad = t._tool_fold_structure({"gene_symbol": "MC1R"})
    assert bad["ok"] is False
    assert "which organism" in bad["error"].lower()


def test_fold_structure_accepts_any_organism(monkeypatch):
    """Regression: bacteria and yeast must reach the resolver untouched."""
    seen = {}

    def fake(organism, gene_symbol):
        seen["args"] = (organism, gene_symbol)
        return {"ok": True, "uniprot": "Q99ZW2", "alphafold_url": "u",
                "alphafold_page": "p"}

    from dee.core import resolve as _resolve
    monkeypatch.setattr(_resolve, "resolve_uniprot", fake)
    out = t._tool_fold_structure({"gene_symbol": "cas9", "organism": "S. pyogenes"})
    assert out["ok"] is True and out["uniprot"] == "Q99ZW2"
    assert seen["args"] == ("s. pyogenes", "cas9")


def test_fold_structure_returns_public_model_reference(monkeypatch):
    seen = {}

    def fake_resolve_uniprot(organism, gene_symbol):
        seen["args"] = (organism, gene_symbol)
        return {"ok": True, "uniprot": "Q01726",
                "alphafold_url": "https://alphafold.ebi.ac.uk/files/AF-Q01726-F1-model_v6.pdb",
                "alphafold_page": "https://alphafold.ebi.ac.uk/entry/Q01726"}

    from dee.core import resolve as _resolve
    monkeypatch.setattr(_resolve, "resolve_uniprot", fake_resolve_uniprot)

    out = t._tool_fold_structure({"gene_symbol": "MC1R", "organism": "human"})
    assert out["ok"] is True
    assert out["uniprot"] == "Q01726"
    assert out["alphafold_url"].startswith("https://alphafold.ebi.ac.uk/")
    assert out["source"] == "AlphaFold DB"
    # ONLY (organism, gene) was sent onward β€” no sequence anywhere in the call.
    assert seen["args"] == ("human", "MC1R")


def test_fold_structure_surfaces_lookup_failure_honestly(monkeypatch):
    from dee.core import resolve as _resolve
    monkeypatch.setattr(_resolve, "resolve_uniprot",
                        lambda o, g: {"ok": False, "error": "No reviewed UniProt entry found for ZZZ (human)."})
    out = t._tool_fold_structure({"gene_symbol": "ZZZ", "organism": "human"})
    assert out["ok"] is False
    assert "ZZZ" in out["error"]


# --------------------------------------------------------------------------- #
# The failure UI must name the actual cause
# --------------------------------------------------------------------------- #
# An audit found AlphaFold-DB up and serving 200s (v6, ~0.3s) while the viewer
# was telling a user "AlphaFold-DB is temporarily unreachable". The headline
# was the WHOLE message, so every possible cause β€” a Mol* bundle that never
# loaded, a stale model version 404, an aborted timeout, a parse failure β€”
# was reported as an EBI outage, and nothing on screen could distinguish them.
def _app_js():
    with open("dee/static/app.js", encoding="utf-8") as fh:
        return fh.read()


def test_the_error_ui_accepts_and_shows_a_reason():
    src = _app_js()
    assert "function _afShowError(host, transient, retry, reason)" in src
    # Shown to the user, and logged in full for whoever is debugging.
    assert "console.warn('[alphafold] '" in src


def test_the_headline_no_longer_asserts_that_ebi_is_down():
    """'did not return the model' describes what we observed. 'is temporarily
    unreachable' is a claim about a third party's uptime that we were making
    without evidence β€” and that the audit showed was often false."""
    src = _app_js()
    assert "AlphaFold-DB is temporarily unreachable" not in src
    assert "AlphaFold-DB did not return the model" in src


def test_every_failure_path_passes_a_reason():
    """A reason parameter nothing populates is decoration. Each call site must
    say which failure it is reporting."""
    src = _app_js()
    for reason in (
        "the 3-D viewer library did not load (CDN or network)",
        "the AlphaFold API has no entry for this accession",
        "every candidate model URL failed to load",
    ):
        assert reason in src, f"missing reason: {reason}"