Files
codex-py/tests/synthesis/test_model.py
Tarik Moussa 3092f1814e fix(synthesis): namespace lead ids by kind to stop collisions (audit C-11)
Each stage (find_connections/gaps/improvements/propose_conjectures) numbered its
leads from seq=1, so connection L-0001, gap L-0001 and improvement L-0001 all
resolved to grounded/L-0001.md. The CLI concatenates them
(connections + gaps + improvements) and write_leads writes in order, so later
kinds silently overwrote earlier ones — survivors = max(per-kind count), not the
sum. Reproduced: 4 distinct leads -> 2 files.

_make_lead_id now takes the kind and emits L-C-/L-G-/L-I-/L-X- prefixes, keeping
same-seq ids distinct across stages. Regression tests cover the format and the
no-collision invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 11:09:24 +02:00

113 lines
3.6 KiB
Python

"""Tests for the Lead / Provenance datamodel (F-13)."""
from __future__ import annotations
from typing import get_args
import pytest
from codex.synthesis import Lead, LeadKind, LeadStatus, Provenance
def test_provenance_requires_bibkey_and_locator() -> None:
"""Provenance has two required positional fields."""
p = Provenance(bibkey="Smith2020", locator="chunk 7")
assert p.bibkey == "Smith2020"
assert p.locator == "chunk 7"
def test_provenance_missing_field_raises() -> None:
"""Provenance without locator must fail to construct (TypeError)."""
with pytest.raises(TypeError):
Provenance(bibkey="Smith2020") # type: ignore[call-arg]
def test_lead_required_fields() -> None:
"""Lead requires id, kind, title, body, provenance, confidence."""
lead = Lead(
id="L-0001",
kind="connection",
title="A connection",
body="Some body text.",
provenance=[Provenance(bibkey="X2020", locator="chunk 1")],
confidence=0.8,
)
assert lead.id == "L-0001"
assert lead.kind == "connection"
assert lead.status == "unverified" # default
assert lead.suggested_validation == "" # default
assert lead.claims == [] # default factory
def test_lead_kind_literals_exact() -> None:
"""Lead kinds are exactly: connection, gap, improvement, conjecture."""
assert set(get_args(LeadKind)) == {"connection", "gap", "improvement", "conjecture"}
def test_lead_status_literals_exact() -> None:
"""Lead statuses are exactly: unverified, spiking, go, no-go."""
assert set(get_args(LeadStatus)) == {"unverified", "spiking", "go", "no-go"}
def test_lead_provenance_can_be_multiple() -> None:
"""A Lead may carry multiple provenance pointers."""
provs = [
Provenance(bibkey="A2020", locator="chunk 1"),
Provenance(bibkey="B2021", locator="chunk 7"),
]
lead = Lead(
id="L-0002",
kind="gap",
title="t",
body="b",
provenance=provs,
confidence=0.5,
)
assert len(lead.provenance) == 2
def test_lead_missing_provenance_field_raises() -> None:
"""Constructing Lead without provenance is a TypeError."""
with pytest.raises(TypeError):
Lead( # type: ignore[call-arg]
id="L-0003",
kind="connection",
title="x",
body="y",
confidence=0.5,
)
def test_lead_empty_provenance_raises() -> None:
"""Constructing Lead with empty provenance list raises ValueError."""
with pytest.raises(ValueError, match="at least one Provenance"):
Lead(
id="L-0004",
kind="connection",
title="x",
body="y",
provenance=[],
confidence=0.5,
)
def test_lead_id_format_helper() -> None:
"""_make_lead_id produces a kind-prefixed, zero-padded L-<K>-XXXX string."""
from codex.synthesis import _make_lead_id
assert _make_lead_id(1, "connection") == "L-C-0001"
assert _make_lead_id(42, "gap") == "L-G-0042"
assert _make_lead_id(9999, "improvement") == "L-I-9999"
assert _make_lead_id(1, "conjecture") == "L-X-0001"
def test_lead_id_namespaced_per_kind_no_collision() -> None:
"""Regression for audit C-11: each stage numbers from 1, so without a kind
prefix connection/gap/improvement all collide on L-0001 and overwrite one
another in grounded/. The prefix must keep same-seq ids distinct.
"""
from codex.synthesis import _make_lead_id
ids = {_make_lead_id(1, k) for k in ("connection", "gap", "improvement", "conjecture")}
assert len(ids) == 4, f"same-seq ids must stay distinct across kinds, got {ids}"