File size: 9,990 Bytes
598a072
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5194826
 
 
 
 
 
 
598a072
5194826
 
 
 
 
 
 
 
 
 
 
 
 
 
598a072
 
5194826
 
598a072
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5194826
 
 
 
 
598a072
 
5194826
598a072
 
 
 
 
 
 
 
 
 
a326839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598a072
 
 
 
 
 
 
3e09cc5
598a072
 
 
 
 
 
 
 
5194826
 
 
 
 
 
 
 
 
 
 
 
598a072
 
a326839
 
 
 
 
 
3e09cc5
a326839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598a072
 
 
 
 
 
 
 
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""Tests for the admin maintenance routes that produce the REAL glass-box
receipts server-side (/api/admin/run-benchmarks, /api/admin/seed-commons) —
run in-Space, where the live ESM-2 model and Supabase credentials already
exist. Both are admin-token gated; both are exercised against the ACTUAL
bundled fixture (dee/data/dms_fixtures/) so a malformed manifest/CSV would
fail here too, not just in production.
"""
import json
import re
import types
from pathlib import Path

import pandas as pd
import pytest

from dee import server
from dee.core.dms_seed import parse_proteingym_csv

ADMIN_TOKEN = "test-admin-token-xyz"


@pytest.fixture
def client():
    app = server.create_app()
    app.config.update(TESTING=True)
    return app.test_client()


def _with_admin(monkeypatch):
    monkeypatch.setenv("TURINGDNA_ADMIN_TOKEN", ADMIN_TOKEN)


def _fixtures_dir():
    return Path(server.__file__).resolve().parent / "data" / "dms_fixtures"


# --------------------------------------------------------------------------- #
# auth gate — shared shape for both routes
# --------------------------------------------------------------------------- #
def test_run_benchmarks_requires_admin_token(client, monkeypatch):
    _with_admin(monkeypatch)
    r = client.post("/api/admin/run-benchmarks")
    assert r.status_code == 403
    r2 = client.post("/api/admin/run-benchmarks", headers={"X-Admin-Token": "wrong"})
    assert r2.status_code == 403


def test_seed_commons_requires_admin_token(client, monkeypatch):
    _with_admin(monkeypatch)
    r = client.post("/api/admin/seed-commons")
    assert r.status_code == 403


# --------------------------------------------------------------------------- #
# /api/admin/run-benchmarks — real bundled fixture, fake (fast) scorer
# --------------------------------------------------------------------------- #
def test_run_benchmarks_against_real_fixture(client, monkeypatch, tmp_path):
    _with_admin(monkeypatch)
    real_fixtures = _fixtures_dir()   # resolve BEFORE monkeypatching server.__file__ below
    manifest = json.loads((real_fixtures / "manifest.json").read_text(encoding="utf-8"))
    assert len(manifest) >= 3, "expected the 3 bundled + validated DMS assays"

    # Build a per-sequence fake scorer covering EVERY bundled assay's own real
    # labels (synthetic-but-distinct values, index-based) — a genuine smoke
    # test of all 3 production fixtures end to end (real manifest -> real CSV
    # -> predict_additive -> evaluate_dataset -> summarize -> written file),
    # without needing ESM.
    rx = re.compile(r"^([A-Za-z])(\d+)([A-Za-z*])$")
    df_by_seq = {}
    for a in manifest:
        recs = parse_proteingym_csv((real_fixtures / a["csv"]).read_text(encoding="utf-8"))
        assert len(recs) > 50, f"{a['name']} fixture looks too small to be real"
        rows, seen = [], set()
        for i, (lab, _v) in enumerate(recs):
            m = rx.match(lab)
            key = (int(m.group(2)) - 1, m.group(3).upper())
            if key in seen:
                continue
            seen.add(key)
            rows.append({"position": key[0], "wt_aa": m.group(1).upper(),
                         "mut_aa": key[1], "delta_ll": float(i % 37) * 0.1})
        df_by_seq[a["sequence"]] = pd.DataFrame(rows)

    monkeypatch.setattr(server._scoring, "get_scorer", lambda *a, **kw: "the-scorer")
    monkeypatch.setattr(server._scoring, "score_guarded",
                        lambda scorer, seq: df_by_seq[seq])
    # Write the output somewhere disposable so this test never touches the
    # committed dee/data/benchmarks.json.
    monkeypatch.setattr(server, "__file__",
                        str(tmp_path / "server.py"))  # relocates Path(__file__).parent
    (tmp_path / "data").mkdir()
    import shutil
    shutil.copytree(real_fixtures, tmp_path / "data" / "dms_fixtures")

    r = client.post("/api/admin/run-benchmarks",
                    headers={"X-Admin-Token": ADMIN_TOKEN}, json={"model": "small"})
    assert r.status_code == 200
    body = r.get_json()
    assert body["ok"] is True
    assert body["model"] == "small"
    assert body["generated_at"] is not None
    assert body["summary"]["n_datasets"] == len(manifest)
    names = {d["name"] for d in body["datasets"]}
    assert names == {a["name"] for a in manifest}
    for d in body["datasets"]:
        assert d["n"] > 0

    written = json.loads((tmp_path / "data" / "benchmarks.json").read_text(encoding="utf-8"))
    assert written["summary"]["n_datasets"] == len(manifest)


def test_run_benchmarks_missing_fixtures_404(client, monkeypatch, tmp_path):
    _with_admin(monkeypatch)
    monkeypatch.setattr(server, "__file__", str(tmp_path / "server.py"))
    (tmp_path / "data").mkdir()   # no dms_fixtures subdir
    r = client.post("/api/admin/run-benchmarks", headers={"X-Admin-Token": ADMIN_TOKEN})
    assert r.status_code == 404


def test_run_benchmarks_surfaces_per_assay_failure(client, monkeypatch, tmp_path):
    """One assay's scoring blows up (e.g. a real ESM error on the Space) — the
    other two must still complete, and the failure must be visible in the
    response itself (name + error), not just swallowed into a shorter
    'datasets' list with no explanation. This is what a real diagnosis of
    'why did only 1 of 3 datasets come back' should read directly off the
    curl output instead of requiring server-log access."""
    _with_admin(monkeypatch)
    real_fixtures = _fixtures_dir()
    manifest = json.loads((real_fixtures / "manifest.json").read_text(encoding="utf-8"))
    assert len(manifest) >= 3

    def flaky_score_guarded(scorer, seq):
        if len(seq) > 500:   # the two longer real fixtures (PABP 577aa, DLG4 724aa)
            raise RuntimeError("boom: simulated real scoring failure")
        return pd.DataFrame([{"position": 0, "wt_aa": seq[0], "mut_aa": "Z", "delta_ll": 0.1}])

    monkeypatch.setattr(server._scoring, "get_scorer", lambda *a, **kw: "the-scorer")
    monkeypatch.setattr(server._scoring, "score_guarded", flaky_score_guarded)
    monkeypatch.setattr(server, "__file__", str(tmp_path / "server.py"))
    (tmp_path / "data").mkdir()
    import shutil
    shutil.copytree(real_fixtures, tmp_path / "data" / "dms_fixtures")

    r = client.post("/api/admin/run-benchmarks",
                    headers={"X-Admin-Token": ADMIN_TOKEN}, json={"model": "small"})
    assert r.status_code == 200
    body = r.get_json()
    long_assays = [a["name"] for a in manifest if len(a["sequence"]) > 500]
    assert len(long_assays) == 2, "expected exactly PABP + DLG4 to be >500aa"
    assert body["summary"]["n_datasets"] == len(manifest) - len(long_assays)
    assert "failed" in body and len(body["failed"]) == len(long_assays)
    failed_names = {f["name"] for f in body["failed"]}
    assert failed_names == set(long_assays)
    for f in body["failed"]:
        assert "boom: simulated real scoring failure" in f["error"]


# --------------------------------------------------------------------------- #
# /api/admin/seed-commons — real bundled fixture, mocked Supabase write
# --------------------------------------------------------------------------- #
def test_seed_commons_against_real_fixture(client, monkeypatch):
    _with_admin(monkeypatch)
    captured = {}

    def fake_replace(rows, source="user"):
        captured["rows"] = rows
        return {"ok": True}

    monkeypatch.setattr(server._auth, "replace_mutation_priors", fake_replace)
    r = client.post("/api/admin/seed-commons", headers={"X-Admin-Token": ADMIN_TOKEN})
    assert r.status_code == 200
    body = r.get_json()
    assert body["ok"] is True
    assert body["contributing_assays"] == 3
    # 3 independent assays clears the k-anonymity floor (MIN_USERS=3): every
    # single-mutant scan covers close to all 19 substitutions at every
    # position, so most of the 380 possible substitution TYPES show up in
    # all three proteins' data and survive. Proves the privacy floor is
    # exactly a floor, not a permanent block, once there's real independent
    # coverage — and that it produces real, usable rows on real data.
    assert body["substitutions"] > 0
    assert len(captured["rows"]) == body["substitutions"]
    for row in captured["rows"]:
        assert row["n_users"] >= 3   # every kept row backed by all 3 independent assays
        assert ">" in row["substitution"]


def test_seed_commons_survives_one_bad_csv(client, monkeypatch, tmp_path):
    """A single unreadable/corrupt fixture CSV must not 500 the whole request
    (there was previously no try/except around this loop at all) — it should
    degrade to the other assays and report the failure by name."""
    _with_admin(monkeypatch)
    real_fixtures = _fixtures_dir()
    monkeypatch.setattr(server._auth, "replace_mutation_priors", lambda rows, source="user": {"ok": True})
    monkeypatch.setattr(server, "__file__", str(tmp_path / "server.py"))
    (tmp_path / "data").mkdir()
    import shutil
    dest = tmp_path / "data" / "dms_fixtures"
    shutil.copytree(real_fixtures, dest)
    manifest = json.loads((dest / "manifest.json").read_text(encoding="utf-8"))
    broken_name = manifest[0]["name"]
    (dest / manifest[0]["csv"]).unlink()   # simulate a missing/corrupt fixture file

    r = client.post("/api/admin/seed-commons", headers={"X-Admin-Token": ADMIN_TOKEN})
    assert r.status_code == 200
    body = r.get_json()
    assert body["contributing_assays"] == len(manifest) - 1
    assert "failed" in body and len(body["failed"]) == 1
    assert body["failed"][0]["name"] == broken_name


def test_seed_commons_gated_before_effective_date(client, monkeypatch):
    _with_admin(monkeypatch)
    import datetime as _dt
    from dee.core import aggregate as _agg
    monkeypatch.setattr(_agg, "EFFECTIVE_DATE", _dt.date(2099, 1, 1))
    r = client.post("/api/admin/seed-commons", headers={"X-Admin-Token": ADMIN_TOKEN})
    assert r.status_code == 423
    assert r.get_json()["error"] == "gated"