File size: 6,629 Bytes
11dde75
 
 
 
 
 
 
 
 
 
 
 
634726e
b108542
 
 
 
 
 
 
11dde75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""E2E test of the agent cycle against a local Postgres.

Network discovery + Gemini are stubbed; everything else is real code:
orchestrator graph, DOI dedupe, crawler-state persistence, grounding
(verify_against_text on a real generated PDF), unit normalization,
classification, pg_mirror inserts, run/event bookkeeping.
"""
import os
import sys

os.environ.setdefault("AGENT_WORK_DIR", "/tmp/agent_test")
os.environ["GEMINI_API_KEY"] = "test-key-not-used"
os.environ["AGENT_USE_NTRS"] = "0"      # tests stub the lanes; never touch the network
# No network in tests: the live sources added Sep 2026 (Semantic Scholar
# bulk, OpenAlex topic walk, datasheet seed crawl) are off, and the query
# grid is not seeded so the frontier is the 8 builtin intents.
os.environ["AGENT_USE_S2"] = "0"
os.environ["AGENT_USE_OPENALEX_TOPICS"] = "0"
os.environ["AGENT_USE_DATASHEETS"] = "0"
os.environ["AGENT_SEED_QUERY_GRID"] = "0"

import fitz
import pdf_crawler
import extraction
import batch_ingest
from agent import agdb, config as C, orchestrator

C.GEMINI_API_KEY = "test-key-not-used"

QUOTE = "The tensile modulus of PEEK-CF30 was measured as 24.5 GPa at 23 C."


def make_pdf() -> bytes:
    doc = fitz.open()
    page = doc.new_page()
    page.insert_text((72, 100), "PEEK CF30 composite datasheet")
    page.insert_text((72, 130), QUOTE)
    data = doc.tobytes()
    doc.close()
    return data


FAKE_PDF = make_pdf()


def fake_search_openalex(query, limit):
    yield pdf_crawler.Candidate(
        title="Tensile behavior of carbon fiber PEEK thermoplastic composite laminates",
        pdf_url="https://example.org/peek_cf30.pdf", source="openalex",
        query=query, doi="10.9999/aim.test.0001", year="2026",
        abstract="thermoplastic composite tensile modulus carbon fiber PEEK datasheet")
    # duplicate DOI from a second query/source — must be deduped pre-download
    yield pdf_crawler.Candidate(
        title="Duplicate DOI variant", pdf_url="https://example.org/peek_dup.pdf",
        source="openalex", query=query, doi="10.9999/aim.test.0001", year="2026",
        abstract="thermoplastic composite tensile carbon fiber")


def fake_search_arxiv(query, limit):
    return iter(())


def fake_download(cand, pdf_dir, state):
    import hashlib
    if cand.pdf_url in state.seen_urls:
        return None
    state.seen_urls.add(cand.pdf_url)
    sha = hashlib.sha256(FAKE_PDF).hexdigest()
    if sha in state.seen_hashes:
        return None
    state.seen_hashes.add(sha)
    fname = f"{cand.source}_{pdf_crawler.slugify(cand.title)}_{sha[:8]}.pdf"
    (pdf_dir / fname).write_bytes(FAKE_PDF)
    return {"filename": fname, "title": cand.title, "doi": cand.doi,
            "url": cand.pdf_url, "year": cand.year, "source": cand.source,
            "sha256": sha, "query": cand.query, "bytes": len(FAKE_PDF)}


def fake_extract(pdf_bytes, filename, api_key):
    mat = extraction.Material(
        material_name="Polyetheretherketone 30% Carbon Fiber",
        material_abbreviation="PEEK-CF30", material_class="Composite",
        matrix="PEEK", fiber="Carbon", fiber_volume_fraction="30%",
        properties=[
            extraction.Property(
                section="Mechanical", property_name="Tensile Modulus",
                value_raw="24.5", value_num=24.5, unit="GPa",
                test_condition="23 C", source_quote=QUOTE, page=1),
            extraction.Property(
                section="Mechanical", property_name="Tensile Strength",
                value_raw="99999", value_num=99999.0, unit="MPa",
                test_condition="", source_quote="not in the pdf text at all",
                page=1),
        ])
    return extraction.Extraction(materials=[mat], doc_status="ok")


# --- monkeypatch ------------------------------------------------------------
pdf_crawler.search_openalex = fake_search_openalex
pdf_crawler.search_arxiv = fake_search_arxiv
pdf_crawler.download_pdf = fake_download
batch_ingest.extract_from_pdf = fake_extract

# --- run two cycles ---------------------------------------------------------
orchestrator.bootstrap()
m1 = orchestrator.run_cycle(trigger="test")
print("cycle 1 metrics:", m1)
m2 = orchestrator.run_cycle(trigger="test")
print("cycle 2 metrics:", m2)

# --- assertions -------------------------------------------------------------
conn = agdb.connect()
fails = []


def check(name, cond):
    print(("PASS " if cond else "FAIL ") + name)
    if not cond:
        fails.append(name)


try:
    with conn.cursor() as cur:
        cur.execute('SELECT material_name, status, value_si, unit_canonical, '
                    'source_sha1, prompt_version FROM "Composites_materials"')
        rows = cur.fetchall()
        cur.execute("SELECT count(*) FROM agent_runs WHERE status = 'ok'")
        n_runs = cur.fetchone()[0]
        cur.execute("SELECT doi, ingest_status, rows_inserted FROM agent_doi_seen")
        doi_rows = cur.fetchall()
        cur.execute("SELECT count(*) FROM agent_events")
        n_events = cur.fetchone()[0]

    check("cycle1 downloaded exactly 1 (dup DOI deduped in-cycle)",
          m1.get("downloaded") == 1)
    check("cycle1 inserted 2 rows", m1.get("rows_inserted") == 2)
    check("cycle1 flagged 1 row (out-of-range strength)",
          m1.get("rows_flagged") == 1)
    check("cycle2 downloaded 0 (DOI already in registry)",
          m2.get("downloaded") == 0)
    check("cycle2 skipped >=1 by DOI", m2.get("skipped_doi", 0) >= 1)
    check("2 ok runs recorded", n_runs == 2)
    check("agent_doi_seen has exactly 1 DOI",
          len(doi_rows) == 1 and doi_rows[0][0] == "10.9999/aim.test.0001")
    check("DOI marked ingested with 2 rows",
          doi_rows[0][1] == "ingested" and doi_rows[0][2] == 2)
    check("composite routed to Composites_materials, 2 rows", len(rows) == 2)
    ok_row = [r for r in rows if r[1] == "ok"]
    flg = [r for r in rows if r[1] != "ok"]
    check("grounded modulus row is status ok", len(ok_row) == 1)
    check("modulus value_si ≈ 24.5e9 Pa",
          ok_row and abs(ok_row[0][2] - 24.5e9) < 1e6)
    check("modulus unit_canonical is GPa", ok_row and ok_row[0][3] == "GPa")
    check("bogus strength row flagged", len(flg) == 1 and flg[0][1] in
          ("out_of_range", "unverified"))
    check("prompt_version stamped", all(r[5] for r in rows))
    check("events logged", n_events >= 8)

    state = agdb.get_state(conn, "crawler_state", {})
    check("crawler seen-set persisted to DB",
          "https://example.org/peek_cf30.pdf" in state.get("seen_urls", []))
finally:
    conn.close()

print("\n%d checks failed" % len(fails))
sys.exit(1 if fails else 0)