File size: 7,179 Bytes
4939f56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests for techspec.md Β§3 β€” page capture + final synthesis."""

import asyncio

import pytest

from app.services.page_capture import extract_page_content, capture_page, _last_hit
from app.services import final_synthesis as fs


# ── HTML extraction (stdlib-only parser) ─────────────────────────────────────

_FIXTURE = """
<html><head><title>UniProt | P04637 test</title>
<meta property="og:image" content="https://img.example.org/og.png">
</head><body>
<script>var junk = "<h2>not a heading</h2>";</script>
<h2>Function</h2><p>Tumor suppressor with <b>many</b> roles.</p>
<h3>Subcellular location</h3><p>Nucleus.</p>
<figure><img src="https://img.example.org/fig1.png"></figure>
</body></html>
"""


def test_extract_title_sections_figures():
    out = extract_page_content(_FIXTURE)
    assert out["title"] == "UniProt | P04637 test"
    assert "Tumor suppressor" in out["text_sections"][0]["text"]
    assert out["text_sections"][0]["heading"] == "Function"
    assert any("Subcellular" in (s["heading"] or "") for s in out["text_sections"])
    assert "https://img.example.org/og.png" in out["figure_urls"]
    assert "https://img.example.org/fig1.png" in out["figure_urls"]
    # script-tag contents must not leak into sections
    assert all("junk" not in s["text"] for s in out["text_sections"])


def test_extract_handles_empty_html():
    out = extract_page_content("<html><body></body></html>")
    assert out["title"] is None
    assert out["text_sections"] == []
    assert out["figure_urls"] == []


# ── Capture persistence + failure honesty ────────────────────────────────────


class _FakeTable:
    def __init__(self, store):
        self.store = store

    def upsert(self, row, on_conflict=None):
        self.row = row
        return self

    def execute(self):
        self.store.append(self.row)
        return self


@pytest.mark.asyncio
async def test_capture_success_persists_row(monkeypatch):
    class _Resp:
        status_code = 200
        text = _FIXTURE

        def raise_for_status(self):
            pass

    class _Client:
        def __init__(self, *a, **k):
            pass

        async def __aenter__(self):
            return self

        async def __aexit__(self, *a):
            return False

        async def get(self, url, headers=None):
            return _Resp()

    rows = []
    monkeypatch.setattr("app.services.page_capture._get_supabase", lambda: type("SB", (), {"table": staticmethod(lambda name: _FakeTable(rows))})())
    monkeypatch.setattr("app.services.page_capture.httpx.AsyncClient", _Client)
    monkeypatch.setattr("app.services.page_capture.MIN_INTERVAL_S", 0.0)
    _last_hit.clear()

    row = await capture_page("11111111-2222-3333-4444-555555555555", "uniprot", "https://www.uniprot.org/uniprotkb/P04637", user_id="u1")
    assert row["fetch_status"] == "captured"
    assert row["source"] == "uniprot"
    assert len(rows) == 1


@pytest.mark.asyncio
async def test_capture_failure_still_records_honest_row(monkeypatch):
    class _Boom:
        def __init__(self, *a, **k):
            pass

        async def __aenter__(self):
            raise ConnectionError("down")

        async def __aexit__(self, *a):
            return False

    rows = []
    monkeypatch.setattr("app.services.page_capture._get_supabase", lambda: type("SB", (), {"table": staticmethod(lambda name: _FakeTable(rows))})())
    monkeypatch.setattr("app.services.page_capture.httpx.AsyncClient", _Boom)
    monkeypatch.setattr("app.services.page_capture.MIN_INTERVAL_S", 0.0)

    row = await capture_page("11111111-2222-3333-4444-555555555555", "rcsb", "https://www.rcsb.org/structure/1UBQ")
    assert row["fetch_status"] == "failed"
    assert "down" in row["error_note"]
    assert len(rows) == 1  # the attempt is still recorded


@pytest.mark.asyncio
async def test_per_host_rate_limit_enforced(monkeypatch):
    intervals = []

    async def fake_sleep(s):
        intervals.append(s)

    monkeypatch.setattr("app.services.page_capture.asyncio.sleep", fake_sleep)
    from app.services.page_capture import _throttle
    _last_hit.clear()

    await _throttle("example.org")          # first hit: no wait
    assert intervals == []
    await _throttle("example.org")          # immediate repeat: must be throttled
    assert len(intervals) >= 1 and intervals[0] > 0
    n_after_second = len(intervals)
    await _throttle("other.example.org")    # different host: not throttled
    assert len(intervals) == n_after_second


# ── Final synthesis ───────────────────────────────────────────────────────────


def test_findings_thread_confidence_tier():
    context = {
        "query": {"confidence": "homolog"},
        "blast": {"count": 1, "top_hit": {"accession": "P04637", "description": "Cellular tumor antigen"}},
        "uniprot": {"accession": "P04637", "full_name": "Cellular tumor antigen p53"},
    }
    report = fs.synthesize_sync(context)
    tiers = {f["confidence_tier"] for f in report["findings"]}
    assert tiers == {"homolog"}
    assert any("Cellular tumor antigen p53" in f["claim"] for f in report["findings"])
    assert any("homolog" in c.lower() for c in report["caveats"])


def test_denovo_report_is_explicit_about_predictions():
    context = {
        "query": {"confidence": "de_novo"},
        "blast": {"count": 0, "top_hit": None},
        "uniprot": {"_de_novo": True, "composition": {"sequence_type": "protein", "length": 120}},
    }
    report = fs.synthesize_sync(context)
    assert "de novo" in report["headline"].lower()
    joined = " ".join(report["caveats"]).lower()
    assert "prediction" in joined
    findings_tools = [f["source_tool"] for f in report["findings"]]
    assert "blast" in findings_tools and "uniprot" in findings_tools


def test_findings_reference_real_pages():
    context = {
        "query": {"confidence": "identified"},
        "blast": {"count": 1, "top_hit": {"accession": "P04637", "description": "p53"}},
        "uniprot": {"accession": "P04637", "full_name": "p53", "pdb_ids": ["1TUP"]},
        "alphafold": {"structure_available": True, "pdb_url": "https://files.rcsb.org/x.pdb", "mean_plddt": 91.2},
    }
    report = fs.synthesize_sync(context)
    by_tool = {f["source_tool"]: f for f in report["findings"]}
    assert by_tool["uniprot"]["page_url"].startswith("https://www.uniprot.org/")
    assert by_tool["blast"]["page_url"].startswith("https://www.ncbi.nlm.nih.gov/protein/P04637")


@pytest.mark.asyncio
async def test_synthesis_mode_is_deterministic_without_llm(monkeypatch):
    async def no_llm(*a, **k):
        return None

    monkeypatch.setattr(fs, "_polish_with_llm", no_llm)
    context = {"query": {"confidence": "identified"}, "blast": {"count": 0, "top_hit": None}}
    report = await fs.synthesize(context)
    assert report["_mode"] == "deterministic"
    assert report["findings"][0]["claim"].startswith("No significant similarity")