Spaces:
Running
Running
| """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 | |
| 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") | |