File size: 6,968 Bytes
9fa9716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b284c7
 
 
 
 
 
 
 
 
 
 
9fa9716
 
7b284c7
 
9fa9716
 
 
 
 
 
 
 
6a3f2e0
 
 
 
9fa9716
6a3f2e0
9fa9716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a3f2e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35fa528
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ac5230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests for the CRISPR provenance layer (dee/core/crispr_methods.py).

These assert the disclosure actually discloses — specifically the two scope
limits a reviewing scientist flagged, which the algorithm code knew about but
the UI never showed: the composite's weighting, and that indel predictions are
neither the real inDelphi model nor tuned to the user's cell type.
"""
import pytest

from dee import server
from dee.core import crispr_methods as cm


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


def test_every_ordered_method_exists_and_is_complete():
    for key in cm.METHOD_ORDER:
        m = cm.METHODS[key]
        for field in ("label", "what", "formula", "basis", "limits"):
            assert m.get(field), f"{key} is missing {field}"
        assert isinstance(m.get("citations"), list)


def test_composite_formula_is_stated_not_hidden():
    # The reviewer's ask: "the weights making the composite score should be
    # transparent."
    #
    # CONTRACT CHANGED 2026-08-01, deliberately. This test used to assert
    # `"no hidden" in basis`, which locked in a false claim: the composite is
    # NOT a plain product. crispr.py has always computed
    # `on_target * (1 - 0.6 * self_off)`, so "no hidden weights and no tuning
    # constants" was the disclosure hiding the one weight there is. The
    # provenance now states the weight and calls it a judgement call; the
    # value is asserted against the engine's own constant in
    # tests/test_crispr_disclosure.py so the two cannot drift apart again.
    from dee.core.crispr import COMPOSITE_OFFTARGET_WEIGHT
    c = cm.METHODS["composite"]
    assert "on_target" in c["formula"] and "self_off" in c["formula"]
    assert str(COMPOSITE_OFFTARGET_WEIGHT) in c["formula"]
    assert "judgement call" in c["basis"]


def test_on_target_does_not_claim_to_be_rule_set_2():
    m = cm.METHODS["on_target"]
    assert "not Doench Rule Set 2" in m["basis"] or "INSPIRED" in m["basis"]
    assert "0.55" in m["limits"]        # the honest correlation range


def test_self_off_declares_its_input_only_scope():
    # Self-off is deliberately input-only; the genome-wide answer lives in the
    # separate "Genome off" column, so this must say so rather than imply the
    # tool has no off-target capability at all.
    m = cm.METHODS["self_off"]
    assert "only the sequence you pasted" in m["limits"]


def test_indels_declare_heuristic_and_cell_type_agnostic():
    # The two things the scientist's review turned on.
    m = cm.METHODS["indels"]
    assert "HEURISTIC" in m["limits"]
    assert "CELL-TYPE-AGNOSTIC" in m["limits"]
    # Name real cell lines so the caveat is concrete, not hand-wavy.
    assert "HEK293" in m["limits"]
    # Credit the model it approximates.
    assert any("inDelphi" in c for c in m["citations"])


def test_methods_route_is_public_and_shaped(client):
    r = client.get("/api/crispr/methods")          # no auth — auditable by anyone
    assert r.status_code == 200
    body = r.get_json()
    assert body["ok"] is True
    assert body["order"] == cm.METHOD_ORDER
    assert set(body["methods"]) == set(cm.METHOD_ORDER)
    assert "off-target" in body["summary"]


# --------------------------------------------------------------------------- #
# Genome off-target: the column that decides whether "consolidation" is real
# --------------------------------------------------------------------------- #
def test_genome_off_is_documented_with_both_scope_limits():
    m = cm.METHODS["genome_off"]
    # Coverage limit: human/mouse are CDS-only, not whole genome.
    assert "CODING SEQUENCE ONLY" in m["limits"]
    assert "intergenic" in m["limits"]
    # Depth limit: only top-ranked guides are screened.
    assert "top-ranked" in m["limits"]
    # Privacy: the guide never leaves the engine.
    assert "never leaves" in m["basis"]


def test_self_off_points_at_genome_search_not_an_external_tool():
    # The old copy sent users to CRISPOR from here; it should now point at
    # the engine's own genome column instead.
    m = cm.METHODS["self_off"]
    assert "Genome off" in m["limits"]
    assert "CRISPOR" not in m["limits"]


def test_genome_offtarget_top_n_is_bounded():
    # The whole reason the feature was unshippable: a ~3.5s query per guide
    # across all 50 candidates. Keep the screened set small and explicit.
    from dee.core.crispr import GENOME_OFFTARGET_TOP_N
    assert 1 <= GENOME_OFFTARGET_TOP_N <= 15


# --------------------------------------------------------------------------- #
# Genome registry: which organisms, and at what coverage
# --------------------------------------------------------------------------- #
def test_full_genome_organisms_are_actually_full_genome():
    from dee.core.offtarget import GENOME_SOURCES, is_organism_ready
    full = {k for k, v in GENOME_SOURCES.items() if v["scope"] == "full genome"}
    # Small enough to index completely — an off-target ANYWHERE is found.
    assert {"ecoli", "yeast", "worm", "fly"} <= full
    for o in full:
        assert is_organism_ready(o)
        assert GENOME_SOURCES[o]["url"].startswith("https://")


def test_mammals_are_declared_cds_only_not_silently_partial():
    from dee.core.offtarget import GENOME_SOURCES
    for o in ("human", "mouse"):
        assert "CDS" in GENOME_SOURCES[o]["scope"]
    # And the user-facing methods must say so, with the reason.
    limits = cm.METHODS["genome_off"]["limits"]
    assert "CODING SEQUENCE ONLY" in limits
    # The reason must be stated (memory), without false precision — the
    # per-site cost was measured at roughly 90-260 B depending on how the
    # baseline is counted, so we claim an order of magnitude, not a figure.
    assert "390 million" in limits and "gigabytes" in limits


def test_index_cache_is_bounded():
    # Six organisms x multi-GB indexes would OOM the Space; the cache must evict.
    from dee.core import offtarget as ot
    assert ot._MAX_CACHED_INDEXES >= 1
    assert callable(ot._evict_if_needed)


# --------------------------------------------------------------------------- #
# B2: a bare gene symbol needs only a species — not an accession
# --------------------------------------------------------------------------- #
def test_symbol_without_organism_returns_actionable_signal():
    from dee.core import resolve as r
    out = r.resolve_target("TP53", organism="")
    assert out["ok"] is False
    assert out.get("needs_organism") is True      # structured, so the UI can offer a pick
    assert out.get("pending") == "TP53"           # echoes the symbol to re-run
    assert "accession" in out["error"].lower()    # explicitly tells them none is needed


def test_pasted_sequence_never_triggers_organism_prompt():
    from dee.core import resolve as r
    out = r.resolve_target("ATGGCC" * 40, organism="")
    assert out["ok"] is True
    assert not out.get("needs_organism")