Files
codex-py/tests/cli/test_cli.py
Tarik Moussa 553ae3995d fix(search): cast query embedding to ::vector in search paper (DQ-4)
`codex search paper` crashed on the live DB with "operator does not exist:
vector <-> double precision[]". The pgvector `<->` operator needs its RHS
typed as vector; peer call sites (mcp_server.py, wiki.py) cast but the CLI
did not. Mocked unit tests never exercise real pgvector, so it went unseen.

Add the cast to both `<->` occurrences and a regression guard asserting the
SQL contains `::vector`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:51:38 +02:00

385 lines
14 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
# Regression guard (DQ-4): the pgvector `<->` operator requires the
# query embedding cast to ::vector. Without it, real Postgres raises
# "operator does not exist: vector <-> double precision[]". Mocked DBs
# don't catch the type error, so assert the cast is in the SQL.
executed_sql = mock_conn.execute.call_args_list[0][0][0]
assert "<->" in executed_sql and "::vector" in executed_sql
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]}"
)