"""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, _detect_conflicts, _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 def test_parse_claims_ignores_markdown_links() -> None: """Markdown hyperlinks like [text](https://example.com) are NOT parsed as claims.""" md = "See the [related work](https://example.com) for details." claims = _parse_claims(md) assert claims == [] def test_parse_claims_ignores_multi_word_bibkey() -> None: """Multi-word 'bibkeys' (spaces inside) are not treated as real citations.""" md = "Some text [not a bibkey here] and more." claims = _parse_claims(md) assert claims == [] # --------------------------------------------------------------------------- # _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 def test_grounding_guard_adversarial_stopword_bypass() -> None: """Adversarial: stopword-3-gram matches are NOT sufficient for grounding. 'the discrete conformal map' appears in the chunk, but this is a stopword phrase. The preceding hallucinated sentences contain NO content-5-gram present in the chunk → grounded=False. """ # The adversarial text from the session-prompt review finding: adversarial_text = ( "The Riemann hypothesis is proven by combining Hodge theory with quantum entanglement. " "Furthermore P=NP holds for hyperbolic tetrahedra. The discrete conformal map" ) claim = Claim( text=adversarial_text, bibkey="Springborn2008", locator="chunk 0", ) result = _run_grounding_guard([claim], FIXTURE_CHUNKS) assert result[0].grounded is False, ( "Stopword 3-gram 'the discrete conformal map' must NOT ground a hallucinated claim" ) def test_grounding_guard_last_sentence_grounded_earlier_hallucination_ignored() -> None: """Only the last sentence is checked for grounding. If the last sentence IS grounded, the earlier hallucinated sentence is irrelevant (it has no citation → it won't be parsed as a Claim at all). This test verifies that a claim whose text = the last sentence grounds correctly. """ # The last sentence of the claim text is directly grounded in the fixture chunk. grounded_sentence = "the hyperbolic volume is V = L(gamma1) + L(gamma2) + L(gamma3)" claim = Claim( text=grounded_sentence, bibkey="Springborn2008", locator="chunk 16", ) result = _run_grounding_guard([claim], FIXTURE_CHUNKS) assert result[0].grounded is True, ( "Last sentence with content-5-gram present in chunk must be grounded" ) def test_grounding_guard_robust_to_trailing_comma() -> None: """A faithful 5-word claim grounds despite a trailing comma (audit R-1). The chunk says "... the volume function V0 is strictly concave on the angle domain." Before edge-punctuation stripping, the claim token "concave," != the chunk token "concave", so the only content-5-gram failed to match and the claim was wrongly marked ungrounded. """ claim = Claim( text="the volume function V0 is strictly concave,", bibkey="Springborn2008", locator="chunk 16", ) result = _run_grounding_guard([claim], FIXTURE_CHUNKS) assert result[0].grounded is True def test_grounding_guard_no_substring_bleed_across_tokens() -> None: """A 5-gram must match whole tokens, not bleed into a longer chunk word (audit C-6). The chunk contains 'subset'; a claim whose first token is 'set' must NOT ground by substring-matching the suffix of 'subset'. """ chunks = [ { "id": 99, "paper_id": "p", "ord": 0, "bibkey": "TestBib", "content": "the subset conformal map energy functional is minimized here", } ] claim = Claim( text="the set conformal map energy functional", bibkey="TestBib", locator="chunk 0", ) result = _run_grounding_guard([claim], 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 def test_inject_cross_refs_skips_inline_code() -> None: """Concept titles inside backtick inline-code spans are NOT replaced.""" concepts = [ Concept( slug="circle-packing", title="circle-packing", aliases=[], ), FIXTURE_CONCEPT, ] # The term "circle-packing" appears both bare (should be replaced) # and inside backticks (should NOT be replaced). text = "Use `circle-packing` for this. See also circle-packing theory." result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function") assert "`circle-packing`" in result # inline-code preserved assert "[[circle-packing]]" in result # bare occurrence replaced def test_inject_cross_refs_skips_math_spans() -> None: """Concept names inside LaTeX math are NOT rewritten to [[slug]] links (audit R-9).""" concepts = [ Concept(slug="energy", title="Energy", aliases=[]), FIXTURE_CONCEPT, ] text = "Prose Energy here. Math $x = Energy^2$ and inline \\(Energy\\) stay literal." result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function") assert "$x = Energy^2$" in result # display/inline $...$ math untouched assert "\\(Energy\\)" in result # \(...\) math untouched assert "Prose [[energy]] here." in result # prose occurrence still linked def test_inject_cross_refs_full_list_even_when_single_concept() -> None: """Cross-ref injection uses the full concept list, not just the compiled concept.""" concepts = [ Concept(slug="circle-packing", title="Circle Packing", aliases=[]), Concept(slug="discrete-conformal-map", title="Discrete Conformal Map", aliases=[]), FIXTURE_CONCEPT, ] # Compiling lobachevsky-function only; text mentions the other two concepts text = "Discrete Conformal Map is used with Circle Packing." result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function") assert "[[circle-packing]]" in result assert "[[discrete-conformal-map]]" 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 mock.wiki_min_grounding_rate = 0.5 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, FIXTURE_CHUNKS, 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, FIXTURE_CHUNKS, 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, FIXTURE_CHUNKS, 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, FIXTURE_CHUNKS, 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, FIXTURE_CHUNKS, 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 # --------------------------------------------------------------------------- # Audit C-4: zero-citation pages must NOT bypass the quarantine # --------------------------------------------------------------------------- def test_zero_citation_page_is_quarantined_not_published( mock_retrieve: None, mock_formulas: None, mock_settings: Path, ) -> None: """A confident-but-uncited LLM response yields zero parseable claims (grounding_rate 0.0). It must be quarantined — not written to wiki/ and marked compiled. Regression for audit C-4: the old ``total_claims > 0`` guard let zero-claim (and LLM-outage empty) pages fall through to wiki/. """ from codex.wiki import compile_all (mock_settings / "concepts.yaml").write_text( textwrap.dedent( """\ concepts: - slug: lobachevsky-function title: Lobachevsky Function aliases: [Milnor Lobachevsky] """ ), encoding="utf-8", ) # No [BibKey] citations anywhere → zero claims → grounding_rate 0.0. uncited = MockLLM( "The Lobachevsky function is obviously central to all of geometry. " "Every result follows from it. This paragraph cites nothing at all." ) report = compile_all(changed_only=False, llm=uncited, output_dir=str(mock_settings)) assert "lobachevsky-function" in report.quarantined assert "lobachevsky-function" not in report.compiled assert not (mock_settings / "lobachevsky-function.md").exists() # --------------------------------------------------------------------------- # 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, chunks: list[Any], **kwargs: Any) -> ConceptPage: nonlocal call_count call_count += 1 return original_compile(concept, chunks, **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, chunks: list[Any], **kwargs: Any) -> ConceptPage: nonlocal call_count call_count += 1 return original_compile(concept, chunks, **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 # --------------------------------------------------------------------------- # LLM error handling (Fix 4) # --------------------------------------------------------------------------- def test_compile_concept_llm_unavailable_returns_empty_page( mock_retrieve: None, mock_formulas: None, mock_settings: Path, ) -> None: """When LLM raises an exception, compile_concept returns an empty ConceptPage (no crash).""" class FailingLLM: def generate(self, prompt: str, model: str) -> str: raise ConnectionError("Ollama unavailable") page = compile_concept( FIXTURE_CONCEPT, FIXTURE_CHUNKS, top_k=6, llm=FailingLLM(), wiki_dir=mock_settings ) assert isinstance(page, ConceptPage) assert page.markdown == "" assert page.claims == [] assert page.chunk_hash != "" # hash still computed from chunks # --------------------------------------------------------------------------- # Quarantine (Fix 2) # --------------------------------------------------------------------------- def test_compile_all_quarantines_low_grounding_page( mock_retrieve: None, mock_formulas: None, mock_settings: Path, ) -> None: """A page with grounding rate < 0.5 goes to wiki/draft/ not wiki/.""" from codex.wiki import compile_all (mock_settings / "concepts.yaml").write_text( textwrap.dedent( """\ concepts: - slug: lobachevsky-function title: Lobachevsky Function aliases: [Milnor Lobachevsky] """ ), encoding="utf-8", ) # Ungrounded LLM output → all claims ungrounded → grounding rate 0.0 < 0.5 llm = MockLLM(UNGROUNDED_LLM_OUTPUT) report = compile_all(changed_only=False, llm=llm, output_dir=str(mock_settings)) draft_dir = mock_settings / "draft" assert draft_dir.exists(), "draft/ directory should have been created" draft_pages = list(draft_dir.glob("*.md")) assert len(draft_pages) >= 1, "Quarantined page should exist in wiki/draft/" assert "lobachevsky-function" in report.quarantined # Normal wiki/ path must NOT exist for this page assert not (mock_settings / "lobachevsky-function.md").exists() def test_compile_all_does_not_quarantine_grounded_page( mock_retrieve: None, mock_formulas: None, mock_settings: Path, ) -> None: """A page with grounding rate >= 0.5 is written to wiki/, not quarantined.""" from codex.wiki import compile_all (mock_settings / "concepts.yaml").write_text( textwrap.dedent( """\ concepts: - slug: lobachevsky-function title: Lobachevsky Function aliases: [Milnor Lobachevsky] """ ), encoding="utf-8", ) llm = MockLLM(GROUNDED_LLM_OUTPUT) report = compile_all(changed_only=False, llm=llm, output_dir=str(mock_settings)) assert "lobachevsky-function" not in report.quarantined assert (mock_settings / "lobachevsky-function.md").exists() # --------------------------------------------------------------------------- # Conflict detection (Fix 9) # --------------------------------------------------------------------------- def test_detect_conflicts_finds_adversative_keywords() -> None: """Chunks from different bibkeys both containing adversative keywords → conflict.""" chunks = [ { "id": 10, "paper_id": "p1", "ord": 0, "content": "The discrete map works well, but diverges in degenerate cases.", "bibkey": "Smith2020", }, { "id": 11, "paper_id": "p2", "ord": 0, "content": "However, the approach from Jones is more stable.", "bibkey": "Jones2019", }, ] conflicts = _detect_conflicts("test-concept", chunks) assert len(conflicts) >= 1 slugs, bk1s, bk2s = zip(*conflicts, strict=True) assert "test-concept" in slugs def test_detect_conflicts_no_conflict_when_single_bibkey() -> None: """Only one bibkey → no conflict possible.""" conflicts = _detect_conflicts("test-concept", FIXTURE_CHUNKS) assert conflicts == [] # --------------------------------------------------------------------------- # append_log quarantine rendering # --------------------------------------------------------------------------- def test_log_append_records_quarantined( mock_settings: Path, ) -> None: """Quarantined slugs appear in the log entry.""" report = CompileReport( compiled=[], quarantined=["some-concept"], ) append_log(report, wiki_dir=mock_settings) content = (mock_settings / "log.md").read_text(encoding="utf-8") assert "some-concept" in content assert "QUARANTINED" in content