Files
codex-py/tests/cli/test_cli.py
Tarik Moussa b87087d3c6 fix(ingest): D-04 bibkey heuristic + D-05a/b/c regression tests + __main__
D-04: add _make_bibkey(paper) — ConcatSurnames+Year, ≤3 authors full list,
      >3 authors FirstEtAlYear. Applied in ingest_paper when paper.bibkey is
      None. ON CONFLICT now fills NULL bibkeys via COALESCE(papers.bibkey,
      EXCLUDED.bibkey) while preserving manually-set values.

D-05b: add codex/__main__.py so `python -m codex` dispatches to the CLI.

D-05c: fix test_ingest.py:75 — rename var to raw_api_citations (the raw
       OpenAlex payload with citing_id=openalex_id) and add regression
       assertion: citing_id written to DB must equal paper.id (DOI), not
       paper.openalex_id.

D-05a (proxy): add TestEntryPoint.test_python_m_codex_help — runs
       `sys.executable -m codex --help` via subprocess, covering the
       non-uv-run entry path. 253 tests green, ruff+mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 03:07:06 +02:00

378 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for codex.cli — Typer command-line interface.
All external calls (ingest, DB, embed, discovery, provenance) are mocked
so the suite runs offline without real API calls.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock, patch
import numpy as np
from typer.testing import CliRunner
from codex.cli import app
from codex.ingest import IngestResult
from codex.models import CodeLink
runner = CliRunner()
class TestIngest:
"""Tests for `codex ingest` command."""
def test_ingest_command(self) -> None:
"""Test basic ingest command with mocked ingest_paper."""
with patch("codex.ingest.ingest_paper") as mock_ingest:
mock_ingest.return_value = IngestResult(
paper_id="2301.07041",
chunks_upserted=5,
citations_upserted=3,
)
result = runner.invoke(app, ["ingest", "2301.07041"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "5 chunks" in result.stdout
assert "3 citations" in result.stdout
def test_ingest_with_rich_flag(self) -> None:
"""Test `ingest --rich` passes rich=True and reports formulas/figures."""
with patch("codex.ingest.ingest_paper") as mock_ingest:
mock_ingest.return_value = IngestResult(
paper_id="2301.07041",
chunks_upserted=5,
citations_upserted=3,
formulas_upserted=12,
figures_upserted=4,
)
result = runner.invoke(app, ["ingest", "2301.07041", "--source", "paper.pdf", "--rich"])
assert result.exit_code == 0
assert "12 formulas" in result.stdout
assert "4 figures" in result.stdout
# Verify rich=True was passed
_, kwargs = mock_ingest.call_args
assert kwargs.get("rich") is True or mock_ingest.call_args[1].get("rich") is True
class TestSearch:
"""Tests for `codex search` subcommands (paper + formula)."""
def test_search_command(self) -> None:
"""Alias: delegates to test_search_paper_command for backwards compat."""
self.test_search_paper_command()
def test_search_paper_command(self) -> None:
"""Test `search paper` command with mocked embedder and DB."""
with (
patch("codex.embed.get_embedder") as mock_embedder,
patch("codex.db.get_conn") as mock_get_conn,
):
# Mock embedder - return numpy array with one row
mock_emb_instance = MagicMock()
mock_emb_instance.encode_dense.return_value = np.array([[0.1, 0.2, 0.3]])
mock_embedder.return_value = mock_emb_instance
# Mock DB connection
mock_conn = MagicMock()
mock_conn.__enter__.return_value = mock_conn
mock_conn.__exit__.return_value = None
mock_conn.execute.return_value.fetchall.return_value = [
{
"id": "2301.07041",
"title": "Test Paper",
"year": 2023,
"distance": 0.123,
},
{
"id": "2301.07042",
"title": "Another Paper",
"year": 2023,
"distance": 0.456,
},
]
mock_get_conn.return_value = mock_conn
result = runner.invoke(app, ["search", "paper", "machine learning", "--limit", "2"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "2301.07042" in result.stdout
assert "Test Paper" in result.stdout
assert "0.123" in result.stdout
def test_search_formula_command(self) -> None:
"""Test `search formula` command with mocked DB."""
with patch("codex.db.get_conn") as mock_get_conn:
mock_conn = MagicMock()
mock_conn.__enter__.return_value = mock_conn
mock_conn.__exit__.return_value = None
mock_conn.execute.return_value.fetchall.return_value = [
{
"paper_id": "2301.07041",
"page": 3,
"raw_latex": r"\sum_{i=1}^{n} x_i",
"context": "summation formula",
"rank": 0.856,
},
]
mock_get_conn.return_value = mock_conn
result = runner.invoke(app, ["search", "formula", "summation", "--limit", "5"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "p.3" in result.stdout
def test_search_formula_no_results(self) -> None:
"""`search formula` with no DB matches prints a 'no results' message."""
with patch("codex.db.get_conn") as mock_get_conn:
mock_conn = MagicMock()
mock_conn.__enter__.return_value = mock_conn
mock_conn.__exit__.return_value = None
mock_conn.execute.return_value.fetchall.return_value = []
mock_get_conn.return_value = mock_conn
result = runner.invoke(app, ["search", "formula", "nonexistent"])
assert result.exit_code == 0
assert "No matching" in result.stdout
class TestDiscoverLeads:
"""Tests for `codex discover leads` command."""
def test_discover_leads_command(self) -> None:
"""Test discover leads command with mocked discovery_leads."""
with patch("codex.discover.discovery_leads") as mock_leads:
mock_leads.return_value = [
{"cited_id": "2301.07041", "pull": 5},
{"cited_id": "2301.07042", "pull": 3},
]
result = runner.invoke(app, ["discover", "leads", "--limit", "2"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "2301.07042" in result.stdout
assert "5×" in result.stdout
assert "3×" in result.stdout
class TestDiscoverCiting:
"""Tests for `codex discover citing` command."""
def test_discover_citing_command(self) -> None:
"""Test discover citing command with mocked citing_papers."""
with patch("codex.discover.citing_papers") as mock_citing:
mock_citing.return_value = ["2301.07041", "2301.07042"]
result = runner.invoke(app, ["discover", "citing", "2301.07043"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "2301.07042" in result.stdout
class TestDiscoverCitedBy:
"""Tests for `codex discover cited-by` command."""
def test_discover_cited_by_command(self) -> None:
"""Test discover cited-by command with mocked cited_by."""
with patch("codex.discover.cited_by") as mock_cited:
mock_cited.return_value = ["2301.07041", "2301.07042"]
result = runner.invoke(app, ["discover", "cited-by", "2301.07043"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "2301.07042" in result.stdout
class TestDiscoverCocited:
"""Tests for `codex discover cocited` command."""
def test_discover_cocited_command(self) -> None:
"""Test discover cocited command with mocked cocited_papers."""
with patch("codex.discover.cocited_papers") as mock_cocited:
mock_cocited.return_value = [
{"paper_id": "2301.07041", "co_citations": 5},
{"paper_id": "2301.07042", "co_citations": 3},
]
result = runner.invoke(app, ["discover", "cocited", "2301.07043"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "2301.07042" in result.stdout
assert "5×" in result.stdout
assert "3×" in result.stdout
class TestProvScan:
"""Tests for `codex provenance scan` command."""
def test_prov_scan_command(self) -> None:
"""Test provenance scan command with mocked scan_cite_tags."""
with patch("codex.provenance.scan_cite_tags") as mock_scan:
mock_scan.return_value = [
{
"file": "src/main.cpp",
"line": "42",
"symbol": "compute_embedding",
"bibkey": "2301.07041",
},
{
"file": "src/utils.h",
"line": "10",
"symbol": "helper_func",
"bibkey": "2301.07042",
},
]
result = runner.invoke(app, ["provenance", "scan", "/path/to/src"])
assert result.exit_code == 0
assert "src/main.cpp" in result.stdout
assert "2301.07041" in result.stdout
assert "compute_embedding" in result.stdout
class TestProvAddLink:
"""Tests for `codex provenance add-link` command."""
def test_prov_add_link_command(self) -> None:
"""Test provenance add-link command with mocked add_code_link."""
with patch("codex.provenance.add_code_link") as mock_add:
mock_link = CodeLink(
id=1,
symbol="my_func",
paper_id="2301.07041",
role="core_algorithm",
note="Primary reference",
added_at=datetime(2023, 1, 1),
)
mock_add.return_value = mock_link
result = runner.invoke(
app,
[
"provenance",
"add-link",
"my_func",
"--paper-id",
"2301.07041",
"--role",
"core_algorithm",
"--note",
"Primary reference",
],
)
assert result.exit_code == 0
assert "Created code_link" in result.stdout
assert "my_func" in result.stdout
assert "2301.07041" in result.stdout
class TestProvLinks:
"""Tests for `codex provenance links` command."""
def test_prov_links_command(self) -> None:
"""Test provenance links command with mocked list_code_links."""
with patch("codex.provenance.list_code_links") as mock_links:
mock_links.return_value = [
CodeLink(
id=1,
symbol="my_func",
paper_id="2301.07041",
role="core",
note=None,
added_at=datetime(2023, 1, 1),
),
CodeLink(
id=2,
symbol="another_func",
paper_id="2301.07042",
role="utility",
note=None,
added_at=datetime(2023, 1, 2),
),
]
result = runner.invoke(app, ["provenance", "links"])
assert result.exit_code == 0
assert "[1]" in result.stdout
assert "[2]" in result.stdout
assert "my_func" in result.stdout
assert "another_func" in result.stdout
class TestProvBib:
"""Tests for `codex provenance bib` command."""
def test_prov_bib_command(self) -> None:
"""Test provenance bib command with mocked export_bib."""
with patch("codex.provenance.export_bib") as mock_export:
mock_export.return_value = "@article{2301.07041,\n title = {Test},\n}"
result = runner.invoke(app, ["provenance", "bib"])
assert result.exit_code == 0
assert "@article" in result.stdout
assert "2301.07041" in result.stdout
def test_prov_bib_with_output_file(self, tmp_path: Any) -> None:
"""Test provenance bib command writing to file."""
with patch("codex.provenance.export_bib") as mock_export:
mock_export.return_value = "@article{2301.07041,\n title = {Test},\n}"
output_file = tmp_path / "refs.bib"
result = runner.invoke(app, ["provenance", "bib", "--output", str(output_file)])
assert result.exit_code == 0
assert "Wrote" in result.stdout
assert output_file.exists()
assert "@article" in output_file.read_text()
class TestAsk:
"""Tests for `codex ask` command."""
def test_ask_command_exits_1(self) -> None:
"""Test that ask command exits with code 1 (not yet implemented)."""
result = runner.invoke(app, ["ask", "what is machine learning?"])
assert result.exit_code == 1
assert "not yet implemented" in result.stdout or "not yet implemented" in result.stderr
# ---------------------------------------------------------------------------
# D-05a/b — Entry-point smoke tests (python -m codex, no uv run)
# ---------------------------------------------------------------------------
class TestEntryPoint:
"""Verify that `python -m codex` works without `uv run` (D-05a/b)."""
def test_python_m_codex_help(self) -> None:
"""`python -m codex --help` exits 0 and prints usage."""
import subprocess
import sys
proc = subprocess.run(
[sys.executable, "-m", "codex", "--help"],
capture_output=True,
text=True,
)
assert proc.returncode == 0, f"Non-zero exit: {proc.stderr}"
assert "Usage" in proc.stdout or "usage" in proc.stdout.lower(), (
f"No usage in output: {proc.stdout[:200]}"
)