Committed code predated the line-length=100 ruff config; this brings the five drifted files into compliance. No logic change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
178 lines
5.8 KiB
Python
178 lines
5.8 KiB
Python
"""Tests for the ``codex synthesis`` CLI command group."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from codex.cli import app
|
|
from codex.synthesis import Lead, Provenance
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def _grounded_lead(lead_id: str = "L-0001") -> Lead:
|
|
return Lead(
|
|
id=lead_id,
|
|
kind="connection",
|
|
title="Connection — X ⇄ Y",
|
|
body="Some grounded body. [X2020 #chunk 1]",
|
|
provenance=[Provenance(bibkey="X2020", locator="chunk 1")],
|
|
confidence=0.8,
|
|
)
|
|
|
|
|
|
def _conjecture(lead_id: str = "L-0002") -> Lead:
|
|
return Lead(
|
|
id=lead_id,
|
|
kind="conjecture",
|
|
title="Bold guess",
|
|
body="Hypothetical.",
|
|
provenance=[Provenance(bibkey="X2020", locator="chunk 1")],
|
|
confidence=0.0,
|
|
suggested_validation="Run a falsification spike.",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_leads_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
|
"""Point Settings.leads_dir at tmp_path/leads for CLI tests."""
|
|
from codex.config import Settings
|
|
|
|
leads_path = tmp_path / "leads"
|
|
|
|
settings = MagicMock(spec=Settings)
|
|
settings.leads_dir = str(leads_path)
|
|
settings.synthesis_llm_model = "test-model"
|
|
settings.synthesis_llm_url = None
|
|
settings.ollama_base_url = "http://localhost:11434"
|
|
settings.synthesis_top_k = 6
|
|
settings.synthesis_min_grounded_ratio = 0.5
|
|
settings.synthesis_gap_min_coverage = 2
|
|
|
|
monkeypatch.setattr("codex.cli.get_settings", lambda: settings, raising=False)
|
|
monkeypatch.setattr("codex.synthesis.get_settings", lambda: settings)
|
|
monkeypatch.setattr("codex.config.get_settings", lambda: settings, raising=False)
|
|
return leads_path
|
|
|
|
|
|
def test_synthesis_leads_invokes_three_finders(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
isolated_leads_dir: Path,
|
|
) -> None:
|
|
"""`codex synthesis leads --lib-path` calls connection + gap + improvement."""
|
|
calls: list[str] = []
|
|
|
|
def fake_connections(*, top_k: int, llm: object) -> list[Lead]:
|
|
calls.append("connection")
|
|
return [_grounded_lead("L-0001")]
|
|
|
|
def fake_gaps(
|
|
*,
|
|
lib_path: str | None = None,
|
|
llm: object,
|
|
topics: list[str] | None = None,
|
|
) -> list[Lead]:
|
|
calls.append("gap")
|
|
return []
|
|
|
|
def fake_improvements(lib_path: str, *, top_k: int, llm: object) -> list[Lead]:
|
|
calls.append("improvement")
|
|
return []
|
|
|
|
monkeypatch.setattr("codex.synthesis.find_connections", fake_connections)
|
|
monkeypatch.setattr("codex.synthesis.find_gaps", fake_gaps)
|
|
monkeypatch.setattr("codex.synthesis.find_improvements", fake_improvements)
|
|
monkeypatch.setattr("codex.synthesis.default_llm_client", lambda: object())
|
|
|
|
result = runner.invoke(app, ["synthesis", "leads", "--lib-path", "/tmp/fake"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert calls == ["connection", "gap", "improvement"]
|
|
assert (isolated_leads_dir / "grounded" / "L-0001.md").exists()
|
|
|
|
|
|
def test_synthesis_leads_without_lib_path_skips_improvement(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
isolated_leads_dir: Path,
|
|
) -> None:
|
|
"""Without --lib-path, the improvement finder must be skipped (graceful)."""
|
|
calls: list[str] = []
|
|
|
|
monkeypatch.setattr(
|
|
"codex.synthesis.find_connections",
|
|
lambda *, top_k, llm: (calls.append("connection"), [])[1],
|
|
)
|
|
monkeypatch.setattr(
|
|
"codex.synthesis.find_gaps",
|
|
lambda *, lib_path=None, llm, topics=None: (calls.append("gap"), [])[1],
|
|
)
|
|
monkeypatch.setattr(
|
|
"codex.synthesis.find_improvements",
|
|
lambda *args, **kwargs: (calls.append("improvement"), [])[1],
|
|
)
|
|
monkeypatch.setattr("codex.synthesis.default_llm_client", lambda: object())
|
|
|
|
result = runner.invoke(app, ["synthesis", "leads"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "improvement" not in calls
|
|
assert "connection" in calls
|
|
assert "gap" in calls
|
|
|
|
|
|
def test_synthesis_conjectures_writes_to_conjectures_dir(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
isolated_leads_dir: Path,
|
|
) -> None:
|
|
"""`codex synthesis conjectures` lands files under leads/conjectures/."""
|
|
monkeypatch.setattr(
|
|
"codex.synthesis.propose_conjectures",
|
|
lambda *, top_k, llm: [_conjecture("L-0042")],
|
|
)
|
|
monkeypatch.setattr("codex.synthesis.default_llm_client", lambda: object())
|
|
|
|
result = runner.invoke(app, ["synthesis", "conjectures"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert (isolated_leads_dir / "conjectures" / "L-0042.md").exists()
|
|
assert not (isolated_leads_dir / "grounded" / "L-0042.md").exists()
|
|
|
|
|
|
def test_synthesis_report_separates_sections(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
isolated_leads_dir: Path,
|
|
) -> None:
|
|
"""`codex synthesis report` shows grounded and conjectures in distinct headers."""
|
|
from codex.synthesis import write_leads
|
|
|
|
write_leads(
|
|
[_grounded_lead("L-0001"), _conjecture("L-0002")],
|
|
str(isolated_leads_dir),
|
|
)
|
|
|
|
result = runner.invoke(app, ["synthesis", "report"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "GROUNDED LEADS" in result.stdout
|
|
assert "CONJECTURES" in result.stdout
|
|
# Visible separation: GROUNDED header strictly precedes CONJECTURES header
|
|
assert result.stdout.index("GROUNDED LEADS") < result.stdout.index("CONJECTURES")
|
|
# UNVERIFIED tag in the conjectures header
|
|
assert "UNVERIFIED" in result.stdout
|
|
|
|
|
|
def test_synthesis_report_empty(
|
|
isolated_leads_dir: Path,
|
|
) -> None:
|
|
"""`codex synthesis report` with no leads still prints both sections."""
|
|
result = runner.invoke(app, ["synthesis", "report"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "GROUNDED LEADS" in result.stdout
|
|
assert "CONJECTURES" in result.stdout
|
|
assert "(none)" in result.stdout
|