"""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 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 class TestSearch: """Tests for `codex search` command.""" def test_search_command(self) -> None: """Test search 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", "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 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="2023-01-01T00:00:00Z", ) 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="2023-01-01T00:00:00Z", ), CodeLink( id=2, symbol="another_func", paper_id="2301.07042", role="utility", note=None, added_at="2023-01-02T00:00:00Z", ), ] 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