Files
codex-py/tests/wiki/test_compile.py
Tarik Moussa aee00515f4 feat(wiki): concept schema loader + concepts.yaml (F-12)
Adds wiki/concepts.yaml (5 curated seeds: discrete-conformal-map,
circle-packing, lobachevsky-function, discrete-yamabe-flow,
hyperideal-tetrahedron) and codex/wiki.py with full load_concepts()
implementation (Concept dataclass, YAML parser, graceful empty list).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 20:22:51 +02:00

511 lines
16 KiB
Python

"""Tests for compile_concept, compile_all, write_index, append_log.
DB and LLM are fully mocked — no network or database access required.
"""
from __future__ import annotations
import textwrap
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
import pytest
from codex.wiki import (
Claim,
CompileReport,
Concept,
ConceptPage,
_chunk_hash,
_inject_cross_refs,
_parse_claims,
_run_grounding_guard,
append_log,
compile_concept,
write_index,
)
# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------
FIXTURE_CHUNKS: list[dict[str, Any]] = [
{
"id": 1,
"paper_id": "springborn-2008",
"ord": 16,
"content": (
"For an ideal tetrahedron with dihedral angles the hyperbolic volume is "
"V = L(gamma1) + L(gamma2) + L(gamma3). "
"The volume function V0 is strictly concave on the angle domain."
),
"bibkey": "Springborn2008",
"dist": 0.31,
},
{
"id": 2,
"paper_id": "springborn-2008",
"ord": 0,
"content": (
"A discrete conformal map is defined by logarithmic scale factors u_i "
"at each vertex such that the edge lengths satisfy a compatibility condition."
),
"bibkey": "Springborn2008",
"dist": 0.35,
},
]
FIXTURE_CONCEPT = Concept(
slug="lobachevsky-function",
title="Lobachevsky Function",
aliases=["Milnor Lobachevsky", "Clausen function"],
emphasis="Verwendung in hyperbolischen Volumenformeln (Springborn 2008).",
)
# Grounded LLM output: claim text is a substring of the fixture chunk
_GROUNDED_CLAIM = (
"For an ideal tetrahedron with dihedral angles the hyperbolic volume is"
" V = L(gamma1) + L(gamma2) + L(gamma3). [Springborn2008 #chunk 16]"
)
GROUNDED_LLM_OUTPUT = (
"The Lobachevsky function L appears as the building block of hyperbolic volume.\n"
+ _GROUNDED_CLAIM
+ "\nThe volume function V0 is strictly concave on the angle domain."
" [Springborn2008 #chunk 16]\n"
)
# Ungrounded LLM output: claim text not present in any chunk
UNGROUNDED_LLM_OUTPUT = textwrap.dedent(
"""\
The closed form is L(x) = -integral from 0 to x of log|2 sin t| dt. [Springborn2008 #chunk 99]
"""
)
class MockLLM:
"""Deterministic mock LLM that returns a preset response."""
def __init__(self, response: str) -> None:
self._response = response
def generate(self, prompt: str, model: str) -> str:
return self._response
# ---------------------------------------------------------------------------
# _parse_claims
# ---------------------------------------------------------------------------
def test_parse_claims_finds_citation() -> None:
"""Claims with [BibKey #locator] format are parsed correctly."""
md = "Some result. [Springborn2008 #chunk 16]"
claims = _parse_claims(md)
assert len(claims) == 1
assert claims[0].bibkey == "Springborn2008"
assert claims[0].locator == "chunk 16"
def test_parse_claims_no_citations() -> None:
"""Text without citation markers returns empty list."""
md = "A standalone sentence with no citation."
claims = _parse_claims(md)
assert claims == []
def test_parse_claims_multiple() -> None:
"""Multiple citations in same text are all parsed."""
md = "First claim. [Smith2020 #page 5]\nSecond claim. [Jones2019 #eq 3]\n"
claims = _parse_claims(md)
assert len(claims) == 2
# ---------------------------------------------------------------------------
# _run_grounding_guard
# ---------------------------------------------------------------------------
def test_grounding_guard_marks_grounded_claim() -> None:
"""A claim whose text appears in the cited chunk is marked grounded."""
claim = Claim(
text="the hyperbolic volume is V = L(gamma1) + L(gamma2) + L(gamma3)",
bibkey="Springborn2008",
locator="chunk 16",
)
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is True
def test_grounding_guard_marks_ungrounded_claim() -> None:
"""A claim not present in the cited chunk is marked ungrounded."""
claim = Claim(
text="integral from 0 to x of log 2 sin t dt closed form definition",
bibkey="Springborn2008",
locator="chunk 99",
)
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is False
def test_grounding_guard_ungrounded_when_bibkey_missing() -> None:
"""A claim citing a bibkey not in any chunk is ungrounded."""
claim = Claim(text="some statement", bibkey="UnknownBib2000", locator="page 1")
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is False
# ---------------------------------------------------------------------------
# _inject_cross_refs
# ---------------------------------------------------------------------------
def test_inject_cross_refs_replaces_title() -> None:
"""Occurrences of another concept's title are replaced with [[slug]]."""
concepts = [
Concept(
slug="discrete-conformal-map",
title="Discrete Conformal Map",
aliases=[],
),
FIXTURE_CONCEPT,
]
text = "This is related to Discrete Conformal Map theory."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[discrete-conformal-map]]" in result
def test_inject_cross_refs_skips_current_slug() -> None:
"""The current concept's own title is not replaced."""
concepts = [FIXTURE_CONCEPT]
text = "The Lobachevsky Function is important."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[lobachevsky-function]]" not in result
def test_inject_cross_refs_replaces_alias() -> None:
"""Aliases are also replaced with [[slug]]."""
concepts = [
Concept(
slug="circle-packing",
title="Circle Packing",
aliases=["Koebe-Andreev-Thurston", "circle pattern"],
),
FIXTURE_CONCEPT,
]
text = "The Koebe-Andreev-Thurston theorem gives a packing."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[circle-packing]]" in result
# ---------------------------------------------------------------------------
# compile_concept (full mock)
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_retrieve(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _retrieve_chunks to return deterministic fixture chunks."""
monkeypatch.setattr(
"codex.wiki._retrieve_chunks",
lambda queries, top_k: FIXTURE_CHUNKS,
)
@pytest.fixture
def mock_formulas(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _try_embed_formulas to be a no-op (F-09 not present)."""
monkeypatch.setattr("codex.wiki._try_embed_formulas", lambda concept, chunks: None)
@pytest.fixture
def mock_settings(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Patch get_settings to return a settings pointing at tmp_path."""
from codex.config import Settings
wiki_path = tmp_path / "wiki"
wiki_path.mkdir()
mock = MagicMock(spec=Settings)
mock.wiki_dir = str(wiki_path)
mock.wiki_llm_model = "test-model"
mock.wiki_llm_url = None
mock.ollama_base_url = "http://localhost:11434"
mock.wiki_top_k = 6
monkeypatch.setattr("codex.wiki.get_settings", lambda: mock)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock, raising=False)
return wiki_path
def test_compile_concept_returns_concept_page(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""compile_concept returns a ConceptPage with non-empty markdown."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert isinstance(page, ConceptPage)
assert page.concept.slug == "lobachevsky-function"
assert len(page.markdown) > 0
def test_compile_concept_has_chunk_hash(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""Compiled page has a non-empty chunk_hash."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert len(page.chunk_hash) == 64 # SHA-256 hex
def test_compile_concept_grounded_claim_not_flagged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""A grounded claim does NOT receive the ⚠ marker in the output markdown."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
# When all claims are grounded, no ⚠ in markdown
# (may still have 0 ⚠ if claims found; just check no false positive for grounded)
grounded = [c for c in page.claims if c.grounded]
for claim in grounded:
assert claim.grounded is True
def test_compile_concept_ungrounded_claim_marked(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""An ungrounded claim is marked ⚠ in the page markdown."""
llm = MockLLM(UNGROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
ungrounded = [c for c in page.claims if not c.grounded]
if ungrounded:
assert "" in page.markdown
def test_compile_concept_header_present(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""Page markdown starts with an H1 header for the concept title."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert page.markdown.startswith("# Lobachevsky Function")
# ---------------------------------------------------------------------------
# write_index
# ---------------------------------------------------------------------------
def test_index_creates_index_md(
mock_settings: Path,
) -> None:
"""write_index creates wiki/index.md."""
pages = [
("discrete-conformal-map", "Discrete Conformal Map"),
("circle-packing", "Circle Packing"),
]
write_index(pages, wiki_dir=mock_settings)
index_path = mock_settings / "index.md"
assert index_path.exists()
def test_index_contains_links(
mock_settings: Path,
) -> None:
"""wiki/index.md contains [[slug]] links for each page."""
pages = [
("discrete-conformal-map", "Discrete Conformal Map"),
("circle-packing", "Circle Packing"),
]
write_index(pages, wiki_dir=mock_settings)
content = (mock_settings / "index.md").read_text(encoding="utf-8")
assert "[[discrete-conformal-map]]" in content
assert "[[circle-packing]]" in content
def test_index_contains_header(
mock_settings: Path,
) -> None:
"""wiki/index.md starts with a # Wiki Index header."""
write_index([], wiki_dir=mock_settings)
content = (mock_settings / "index.md").read_text(encoding="utf-8")
assert content.startswith("# Wiki Index")
# ---------------------------------------------------------------------------
# append_log
# ---------------------------------------------------------------------------
def test_log_append_creates_log_md(
mock_settings: Path,
) -> None:
"""append_log creates wiki/log.md if it does not exist."""
report = CompileReport(
compiled=["lobachevsky-function"],
skipped=[],
ungrounded=[],
)
append_log(report, wiki_dir=mock_settings)
log_path = mock_settings / "log.md"
assert log_path.exists()
def test_log_append_does_not_overwrite(
mock_settings: Path,
) -> None:
"""Two calls to append_log accumulate entries — older entry is NOT overwritten."""
report1 = CompileReport(compiled=["first-concept"])
report2 = CompileReport(compiled=["second-concept"])
append_log(report1, wiki_dir=mock_settings)
append_log(report2, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "first-concept" in content
assert "second-concept" in content
def test_log_append_records_ungrounded(
mock_settings: Path,
) -> None:
"""Ungrounded claims appear in the log entry."""
report = CompileReport(
compiled=["lobachevsky-function"],
ungrounded=[("lobachevsky-function", "some ungrounded claim text")],
)
append_log(report, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "some ungrounded claim text" in content
assert "" in content
def test_log_append_records_skipped(
mock_settings: Path,
) -> None:
"""Skipped slugs appear in the log entry."""
report = CompileReport(
compiled=[],
skipped=["circle-packing"],
)
append_log(report, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "circle-packing" in content
# ---------------------------------------------------------------------------
# Idempotency: second compile without chunk change → no rewrite
# ---------------------------------------------------------------------------
def test_idempotent_no_rewrite_when_unchanged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Second compile_all with unchanged chunks skips the concept (changed_only=True)."""
from codex.wiki import compile_all
call_count = 0
original_compile = compile_concept
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
nonlocal call_count
call_count += 1
return original_compile(concept, **kwargs)
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
# Write concepts.yaml into wiki_dir
yaml_content = textwrap.dedent(
"""\
concepts:
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky]
"""
)
(mock_settings / "concepts.yaml").write_text(yaml_content, encoding="utf-8")
llm = MockLLM(GROUNDED_LLM_OUTPUT)
# First compile: should call compile_concept
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
first_count = call_count
# Second compile with same chunks: should skip
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
second_count = call_count
assert first_count >= 1, "First run should compile at least once"
assert second_count == first_count, "Second run should not recompile (unchanged chunks)"
def test_force_all_recompiles_even_when_unchanged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""compile_all with changed_only=False recompiles regardless of hash."""
from codex.wiki import compile_all
call_count = 0
original_compile = compile_concept
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
nonlocal call_count
call_count += 1
return original_compile(concept, **kwargs)
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
yaml_content = textwrap.dedent(
"""\
concepts:
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky]
"""
)
(mock_settings / "concepts.yaml").write_text(yaml_content, encoding="utf-8")
llm = MockLLM(GROUNDED_LLM_OUTPUT)
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
compile_all(changed_only=False, llm=llm, output_dir=str(mock_settings))
assert call_count >= 2, "Force --all should recompile even when unchanged"
# ---------------------------------------------------------------------------
# _chunk_hash
# ---------------------------------------------------------------------------
def test_chunk_hash_is_stable() -> None:
"""Same chunks produce the same hash each time."""
h1 = _chunk_hash(FIXTURE_CHUNKS)
h2 = _chunk_hash(FIXTURE_CHUNKS)
assert h1 == h2
assert len(h1) == 64
def test_chunk_hash_differs_on_content_change() -> None:
"""Different content produces different hash."""
modified = [dict(FIXTURE_CHUNKS[0], content="completely different content"), FIXTURE_CHUNKS[1]]
h1 = _chunk_hash(FIXTURE_CHUNKS)
h2 = _chunk_hash(modified)
assert h1 != h2