feat(tests): scaffold + integration smoke tests (config, db, models, pipeline)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
100
tests/integration/test_pipeline_smoke.py
Normal file
100
tests/integration/test_pipeline_smoke.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""End-to-end smoke test: ingest → discover → provenance flow (all mocked)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
from codex.discover import discovery_leads
|
||||
from codex.ingest import ingest_paper
|
||||
from codex.models import Citation, Paper
|
||||
from codex.provenance import export_bib
|
||||
|
||||
|
||||
def _make_conn_cm(mock_conn: MagicMock) -> Any:
|
||||
"""Return a context-manager factory that yields *mock_conn*."""
|
||||
|
||||
@contextmanager
|
||||
def _cm() -> Any:
|
||||
yield mock_conn
|
||||
|
||||
return _cm
|
||||
|
||||
|
||||
def test_full_pipeline_smoke() -> None:
|
||||
"""ingest_paper → discovery_leads → export_bib all execute without errors (all mocked)."""
|
||||
paper = Paper(id="2301.07041", title="T", openalex_id="W123")
|
||||
citations = [Citation(citing_id="2301.07041", cited_id="2301.99999")]
|
||||
|
||||
# --- mock connection setup ---
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.execute = MagicMock()
|
||||
mock_conn.commit = MagicMock()
|
||||
|
||||
# discovery_leads: conn.execute(...).fetchall() → empty list (no leads)
|
||||
discover_execute_result = MagicMock()
|
||||
discover_execute_result.fetchall.return_value = []
|
||||
|
||||
# export_bib: conn.execute(...).fetchall() → one paper row with bibkey
|
||||
bib_row: dict[str, Any] = {
|
||||
"id": "2301.07041",
|
||||
"bibkey": "paper2023",
|
||||
"title": "T",
|
||||
"authors": ["Author One"],
|
||||
"year": 2023,
|
||||
}
|
||||
bib_execute_result = MagicMock()
|
||||
bib_execute_result.fetchall.return_value = [bib_row]
|
||||
|
||||
# The ingest path calls conn.execute multiple times (paper upsert, etc.).
|
||||
# discovery_leads and export_bib each call conn.execute once and use fetchall.
|
||||
# We use side_effect to route calls: first N calls from ingest return a generic
|
||||
# mock; then we supply discover and bib results in order.
|
||||
_call_count: list[int] = [0]
|
||||
|
||||
def _execute_side_effect(*args: Any, **kwargs: Any) -> MagicMock:
|
||||
_call_count[0] += 1
|
||||
sql: str = str(args[0]) if args else ""
|
||||
if "cited_id NOT IN" in sql:
|
||||
return discover_execute_result
|
||||
if "FROM papers" in sql and "bibkey IS NOT NULL" in sql:
|
||||
return bib_execute_result
|
||||
return MagicMock()
|
||||
|
||||
mock_conn.execute.side_effect = _execute_side_effect
|
||||
|
||||
# cursor() context manager for executemany calls inside ingest
|
||||
mock_cursor = MagicMock()
|
||||
mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor)
|
||||
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
embedder = MagicMock()
|
||||
embedder.dim = 1024
|
||||
embedder.encode_dense.return_value = np.zeros((1, 1024), dtype=np.float32)
|
||||
|
||||
conn_cm = _make_conn_cm(mock_conn)
|
||||
|
||||
with (
|
||||
patch("codex.ingest.openalex.fetch_paper", return_value=paper),
|
||||
patch("codex.ingest.openalex.fetch_citations", return_value=citations),
|
||||
patch("codex.ingest.get_embedder", return_value=embedder),
|
||||
patch("codex.ingest.get_conn", side_effect=conn_cm),
|
||||
patch("codex.discover.get_conn", side_effect=conn_cm),
|
||||
patch("codex.provenance.get_conn", side_effect=conn_cm),
|
||||
):
|
||||
# Step 1: ingest
|
||||
result = ingest_paper("2301.07041")
|
||||
|
||||
# Step 2: discover
|
||||
leads = discovery_leads()
|
||||
|
||||
# Step 3: export bib
|
||||
bib = export_bib(["2301.07041"])
|
||||
|
||||
# --- assertions ---
|
||||
assert result.paper_id == "2301.07041"
|
||||
assert isinstance(leads, list)
|
||||
assert "@article" in bib
|
||||
0
tests/scaffold/__init__.py
Normal file
0
tests/scaffold/__init__.py
Normal file
35
tests/scaffold/test_config.py
Normal file
35
tests/scaffold/test_config.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Smoke tests for codex.config.Settings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from codex.config import Settings, get_settings
|
||||
|
||||
|
||||
def test_settings_defaults() -> None:
|
||||
"""Settings() is constructable without env vars; expected defaults are set."""
|
||||
s = Settings()
|
||||
assert s.database_url == "postgresql://researcher:change_me@localhost:5432/papers"
|
||||
assert s.embedding_model == "BAAI/bge-m3"
|
||||
assert s.embedding_dim == 1024
|
||||
|
||||
|
||||
def test_settings_env_override(monkeypatch: object) -> None:
|
||||
"""DATABASE_URL env var overrides the default database_url."""
|
||||
import pytest
|
||||
|
||||
mp: pytest.MonkeyPatch = monkeypatch # type: ignore[assignment]
|
||||
mp.setenv("DATABASE_URL", "postgresql://x:y@h:5432/db")
|
||||
s = Settings()
|
||||
assert s.database_url == "postgresql://x:y@h:5432/db"
|
||||
|
||||
|
||||
def test_get_settings_is_singleton() -> None:
|
||||
"""get_settings() returns the same object on repeated calls; new object after cache_clear."""
|
||||
get_settings.cache_clear()
|
||||
first = get_settings()
|
||||
second = get_settings()
|
||||
assert first is second
|
||||
|
||||
get_settings.cache_clear()
|
||||
third = get_settings()
|
||||
assert third is not first
|
||||
35
tests/scaffold/test_db.py
Normal file
35
tests/scaffold/test_db.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Smoke tests for codex.db — get_conn and apply_schema."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def test_get_conn_context_manager() -> None:
|
||||
"""get_conn() yields a context manager that calls psycopg.connect and cleans up."""
|
||||
from codex.db import get_conn
|
||||
|
||||
mock_conn = MagicMock()
|
||||
mock_connect = MagicMock()
|
||||
# psycopg.connect is itself used as a context manager inside get_conn
|
||||
mock_connect.return_value.__enter__ = MagicMock(return_value=mock_conn)
|
||||
mock_connect.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
with patch("codex.db.psycopg.connect", mock_connect), get_conn() as conn:
|
||||
assert conn is mock_conn
|
||||
|
||||
mock_connect.assert_called_once()
|
||||
|
||||
|
||||
def test_apply_schema_executes_sql() -> None:
|
||||
"""apply_schema(conn) calls conn.execute and conn.commit."""
|
||||
from codex.db import apply_schema
|
||||
|
||||
mock_conn = MagicMock()
|
||||
|
||||
# Provide a fake schema.sql so Path.read_text succeeds
|
||||
with patch("codex.db.Path.read_text", return_value="CREATE TABLE IF NOT EXISTS test ();"):
|
||||
apply_schema(mock_conn)
|
||||
|
||||
mock_conn.execute.assert_called_once()
|
||||
mock_conn.commit.assert_called_once()
|
||||
34
tests/scaffold/test_models.py
Normal file
34
tests/scaffold/test_models.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Smoke tests for codex.models dataclasses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from codex.models import Chunk, Citation, CodeLink, Paper
|
||||
|
||||
|
||||
def test_paper_defaults() -> None:
|
||||
"""Paper(id=..., title=...) has correct optional-field defaults."""
|
||||
p = Paper(id="2301.00001", title="T")
|
||||
assert p.openalex_id is None
|
||||
assert p.authors == []
|
||||
assert p.year is None
|
||||
|
||||
|
||||
def test_chunk_required_fields() -> None:
|
||||
"""Chunk(paper_id=..., ord=..., content=...) has id=None and embedding=None."""
|
||||
c = Chunk(paper_id="x", ord=0, content="c")
|
||||
assert c.id is None
|
||||
assert c.embedding is None
|
||||
|
||||
|
||||
def test_citation_fields() -> None:
|
||||
"""Citation(citing_id=..., cited_id=...) has context=None."""
|
||||
cit = Citation(citing_id="a", cited_id="b")
|
||||
assert cit.context is None
|
||||
|
||||
|
||||
def test_codelink_fields() -> None:
|
||||
"""CodeLink(symbol=...) has paper_id=None, role=None, and id=None."""
|
||||
cl = CodeLink(symbol="foo")
|
||||
assert cl.paper_id is None
|
||||
assert cl.role is None
|
||||
assert cl.id is None
|
||||
Reference in New Issue
Block a user