Files

590 lines
19 KiB
Python

"""The load stage, exercised against the in-memory store with no server.
Two classes of failure are silent in a vector database and are what most of
these tests aim at. Loading the same corpus twice can leave two copies of a
dose, and every query still succeeds — so idempotency is asserted by point
count, not by inspecting the upsert calls. Mixing two corpus generations or two
models into one collection also raises nothing at query time; every search
returns *something*, just from the wrong material. That is what the manifest
gate exists to make loud, and there is a test per way it can be violated.
The last test runs over the real `chunks.jsonl` when it is present, because a
provenance rule that only holds for hand-written records is not evidence.
"""
import json
from pathlib import Path
import pytest
from ingestion.load import (
ChunkLoader,
CollectionSpec,
CorpusManifest,
CorpusMismatch,
InMemoryVectorStore,
PointCountMismatch,
build_point,
corpus_sha256,
count_chunks,
iter_chunk_records,
manifest_collection,
point_id_for,
read_manifest,
validate_chunk_record,
)
DIMENSIONS = 4
COLLECTION = "duoc_thu_chunks"
CORPUS_SHA = "a" * 64
OTHER_SHA = "b" * 64
MODEL = "amazon.titan-embed-text-v2:0"
REAL_CHUNKS = (
Path(__file__).resolve().parents[1] / "data" / "processed" / "chunks.jsonl"
)
def chunk_record(chunk_id="abacavir__lieu_luong__0", **overrides):
record = {
"schema_version": 4,
"chunk_id": chunk_id,
"drug_id": "abacavir",
"drug_name": "ABACAVIR",
"section_key": "lieu_luong_va_cach_dung",
"section_display_name": "Liều lượng và cách dùng",
"text": "Người lớn: 300 mg, hai lần mỗi ngày.",
"source_text": "Người lớn: 300 mg, hai lần mỗi ngày.",
"heading_physical_page": 100,
"source_page_range": [100, 102],
"printed_page_range": [101, 103],
"atc_codes": ["J05AF06"],
"part_index": 0,
"part_count": 1,
"est_tokens": 14,
"oversized": False,
"chunk_kind": "prose",
"attachments": [],
"has_quarantined_content": False,
}
record.update(overrides)
return record
def vector(seed=0.1):
return [seed] * DIMENSIONS
def manifest(**overrides):
values = {
"corpus_sha256": CORPUS_SHA,
"chunk_count": 3,
"model_id": MODEL,
"dimensions": DIMENSIONS,
"input_kind": "document",
"provider": "titan-v2",
}
values.update(overrides)
return CorpusManifest(**values)
def spec(**overrides):
values = {"name": COLLECTION, "vector_size": DIMENSIONS}
values.update(overrides)
return CollectionSpec(**values)
def loader(store, **overrides):
return ChunkLoader(
store,
overrides.pop("spec", spec()),
overrides.pop("manifest", manifest()),
**overrides,
)
def pairs(count=3):
return [
(chunk_record(chunk_id=f"drug__section__{i}"), vector(0.1 * (i + 1)))
for i in range(count)
]
# --- A5: derived ids and idempotency -------------------------------------
def test_point_id_is_derived_from_chunk_id_and_is_stable():
first = point_id_for("abacavir__lieu_luong__0")
second = point_id_for("abacavir__lieu_luong__0")
assert first == second
assert first != point_id_for("abacavir__lieu_luong__1")
def test_point_id_refuses_an_empty_chunk_id():
with pytest.raises(ValueError, match="chunk_id is required"):
point_id_for(" ")
def test_loading_the_same_corpus_twice_leaves_the_point_count_unchanged():
store = InMemoryVectorStore()
data = pairs(3)
first = loader(store).load(data)
second = loader(store).load(data)
assert first.collection_created is True
assert second.collection_created is False
assert first.collection_count == 3
assert second.collection_count == 3, "a re-run duplicated points"
assert second.points_upserted == 3
def test_a_reloaded_chunk_overwrites_its_own_point_rather_than_adding_one():
store = InMemoryVectorStore()
record = chunk_record()
loader(store).load([(record, vector(0.1))])
edited = chunk_record(text="Người lớn: 600 mg, một lần mỗi ngày.")
loader(store).load([(edited, vector(0.9))])
assert store.count(COLLECTION) == 1
stored = store.retrieve(COLLECTION, point_id_for(record["chunk_id"]))
assert stored.payload["text"] == "Người lớn: 600 mg, một lần mỗi ngày."
assert stored.vector == vector(0.9)
def test_records_are_upserted_in_batches_of_the_configured_size():
store = InMemoryVectorStore()
report = loader(store, batch_size=2).load(pairs(5))
assert report.batches == 3
assert report.points_upserted == 5
assert report.collection_count == 5
def test_batch_size_must_be_positive():
with pytest.raises(ValueError, match="batch_size must be positive"):
ChunkLoader(InMemoryVectorStore(), spec(), manifest(), batch_size=0)
# --- A4: collection shape and payload ------------------------------------
def test_collection_is_created_with_the_declared_size_and_payload_indexes():
store = InMemoryVectorStore()
loader(store).load(pairs(1))
assert store.spec(COLLECTION).vector_size == DIMENSIONS
assert store.spec(COLLECTION).distance == "Cosine"
indexed = dict(store.indexed_fields(COLLECTION))
assert indexed["drug_id"] == "keyword"
assert indexed["section_key"] == "keyword"
assert indexed["atc_codes"] == "keyword"
assert indexed["chunk_kind"] == "keyword"
assert indexed["has_quarantined_content"] == "bool"
def test_payload_carries_every_provenance_field_of_the_chunk_record():
store = InMemoryVectorStore()
record = chunk_record()
loader(store).load([(record, vector())])
payload = store.retrieve(COLLECTION, point_id_for(record["chunk_id"])).payload
assert payload == record
def test_a_field_added_by_a_future_chunker_flows_through_untouched():
store = InMemoryVectorStore()
record = chunk_record(
population_tags=["Người lớn", "Suy thận"], printed_page_range=[101, 103]
)
loader(store).load([(record, vector())])
payload = store.retrieve(COLLECTION, point_id_for(record["chunk_id"])).payload
assert payload["population_tags"] == ["Người lớn", "Suy thận"]
assert payload["printed_page_range"] == [101, 103]
@pytest.mark.parametrize(
"field",
[
"chunk_id",
"drug_id",
"section_key",
"source_page_range",
"printed_page_range",
"text",
],
)
def test_a_chunk_missing_a_required_provenance_field_is_refused(field):
with pytest.raises(ValueError, match="missing required provenance fields"):
validate_chunk_record(chunk_record(**{field: None}))
# --- failing closed on incomplete provenance ------------------------------
#
# Every case below passed an earlier version of this validator. The cost of
# that is not an exception at load time — it is paying for an embedding run and
# then discovering every answer abstains because the chunks cannot be cited.
@pytest.mark.parametrize("field", ["source_page_range", "printed_page_range"])
def test_an_empty_page_range_is_missing_not_present(field):
"""`[] in (None, "")` is False, which is exactly how this slipped through."""
with pytest.raises(ValueError, match="missing required provenance fields"):
validate_chunk_record(chunk_record(**{field: []}))
def test_an_unknown_old_or_future_schema_is_refused_fail_closed():
for version in (3, 5):
with pytest.raises(ValueError, match="supports exactly v4"):
validate_chunk_record(chunk_record(schema_version=version))
def test_a_chunk_with_no_schema_version_at_all_is_refused():
record = chunk_record()
del record["schema_version"]
with pytest.raises(ValueError, match="declares schema_version None"):
validate_chunk_record(record)
@pytest.mark.parametrize(
"value", [[101], [101, 102, 103], "101-103", 101, {"start": 101}]
)
def test_a_page_range_that_is_not_a_pair_is_refused(value):
with pytest.raises(ValueError, match=r"expected a \[start, end\] pair"):
validate_chunk_record(chunk_record(printed_page_range=value))
def test_a_page_range_running_backwards_is_refused():
with pytest.raises(ValueError, match="running backwards"):
validate_chunk_record(chunk_record(printed_page_range=[103, 101]))
def test_a_non_integer_page_is_refused():
with pytest.raises(ValueError, match="non-integer page"):
validate_chunk_record(chunk_record(printed_page_range=[101.5, 103]))
def test_boolean_pages_are_not_accepted_as_python_integers():
with pytest.raises(ValueError, match="non-integer page"):
validate_chunk_record(chunk_record(printed_page_range=[False, True]))
@pytest.mark.parametrize(
("field", "value"),
[("source_page_range", [-1, 0]), ("printed_page_range", [0, 1])],
)
def test_page_ranges_reject_impossible_lower_bounds(field, value):
with pytest.raises(ValueError, match="pages must start"):
validate_chunk_record(chunk_record(**{field: value}))
def test_page_zero_and_false_are_values_not_absences():
"""Physical pages are 0-indexed; a falsiness test would reject real records."""
validate_chunk_record(
chunk_record(
heading_physical_page=0,
source_page_range=[0, 0],
printed_page_range=[1, 1],
has_quarantined_content=False,
oversized=False,
part_index=0,
)
)
def test_a_wrong_sized_vector_is_refused_before_anything_is_upserted():
store = InMemoryVectorStore()
with pytest.raises(ValueError, match="expects 4"):
loader(store).load([(chunk_record(), [0.1, 0.2])])
assert store.count(COLLECTION) == 0
def test_manifest_dimensions_must_agree_with_the_collection_spec():
with pytest.raises(ValueError, match="manifest declares 8 dimensions"):
ChunkLoader(InMemoryVectorStore(), spec(), manifest(dimensions=8))
def test_collection_spec_refuses_a_nonpositive_vector_size():
with pytest.raises(ValueError, match="vector_size must be positive"):
CollectionSpec(name=COLLECTION, vector_size=0)
# --- A6: the corpus binding gate -----------------------------------------
def test_the_manifest_lives_beside_the_data_so_the_point_count_stays_exact():
store = InMemoryVectorStore()
loader(store).load(pairs(3))
assert store.count(COLLECTION) == 3, "the manifest must not inflate the count"
assert store.count(manifest_collection(COLLECTION)) == 1
stored = read_manifest(store, COLLECTION)
assert stored.corpus_sha256 == CORPUS_SHA
assert stored.model_id == MODEL
assert stored.provider == "titan-v2"
def test_a_second_corpus_generation_is_refused_and_nothing_is_written():
store = InMemoryVectorStore()
loader(store).load(pairs(3))
with pytest.raises(CorpusMismatch, match="does not match"):
loader(store, manifest=manifest(corpus_sha256=OTHER_SHA)).load(pairs(2))
assert store.count(COLLECTION) == 3
assert read_manifest(store, COLLECTION).corpus_sha256 == CORPUS_SHA
def test_a_second_model_is_refused_even_when_the_corpus_matches():
store = InMemoryVectorStore()
loader(store).load(pairs(1))
with pytest.raises(CorpusMismatch, match="cohere.embed-v4:0"):
loader(store, manifest=manifest(model_id="cohere.embed-v4:0")).load(pairs(1))
def test_a_query_subspace_vector_is_refused_for_a_document_collection():
store = InMemoryVectorStore()
loader(store).load(pairs(1))
with pytest.raises(CorpusMismatch, match="input kind query"):
loader(store, manifest=manifest(input_kind="query")).load(pairs(1))
def test_a_dimension_change_is_refused():
store = InMemoryVectorStore()
loader(store).load(pairs(1))
eight = CollectionSpec(name=COLLECTION, vector_size=8)
with pytest.raises(CorpusMismatch, match="8 dimensions"):
ChunkLoader(store, eight, manifest(dimensions=8)).load(
[(chunk_record(), [0.1] * 8)]
)
def test_an_existing_collection_with_no_manifest_is_refused():
store = InMemoryVectorStore()
store.create_collection(spec())
store.upsert(COLLECTION, [build_point(chunk_record(), vector())])
with pytest.raises(CorpusMismatch, match="has no manifest"):
loader(store).load(pairs(1))
def test_the_same_corpus_and_model_is_allowed_through():
store = InMemoryVectorStore()
loader(store).load(pairs(3))
report = loader(store).load(pairs(3))
assert report.collection_count == 3
assert report.count_matches is True
# --- the v1 point-count gate ---------------------------------------------
def test_assert_point_count_passes_when_every_chunk_has_exactly_one_point():
store = InMemoryVectorStore()
active = loader(store)
active.load(pairs(3))
assert active.assert_point_count(3) == 3
def test_assert_point_count_raises_when_the_collection_is_short():
store = InMemoryVectorStore()
active = loader(store)
active.load(pairs(3))
with pytest.raises(PointCountMismatch, match="holds 3 points but the corpus has 4"):
active.assert_point_count(4)
# --- mode A: exhaustive filter retrieval ---------------------------------
def section_pairs(drug_id, section_key, parts):
return [
(
chunk_record(
chunk_id=f"{drug_id}__{section_key}__{i}",
drug_id=drug_id,
section_key=section_key,
part_index=i,
part_count=parts,
),
vector(0.1),
)
for i in range(parts)
]
def test_a_filter_returns_every_part_of_a_section_not_a_top_k():
"""The rule mode A exists for: two of five contraindications is worse than none."""
store = InMemoryVectorStore()
loader(store).load(
section_pairs("metformin", "chong_chi_dinh", 5)
+ section_pairs("metformin", "lieu_luong_va_cach_dung", 3)
+ section_pairs("pantoprazol", "chong_chi_dinh", 2)
)
found = store.find_by_payload(
COLLECTION, {"drug_id": "metformin", "section_key": "chong_chi_dinh"}
)
assert len(found) == 5
assert sorted(p.payload["part_index"] for p in found) == [0, 1, 2, 3, 4]
assert {p.payload["drug_id"] for p in found} == {"metformin"}
def test_a_filter_never_leaks_a_neighbouring_drugs_section():
store = InMemoryVectorStore()
loader(store).load(
section_pairs("pantoprazol", "chong_chi_dinh", 2)
+ section_pairs("omeprazol", "chong_chi_dinh", 2)
)
found = store.find_by_payload(
COLLECTION, {"drug_id": "pantoprazol", "section_key": "chong_chi_dinh"}
)
assert len(found) == 2
assert {p.payload["drug_id"] for p in found} == {"pantoprazol"}
def test_a_list_valued_field_matches_on_any_element():
store = InMemoryVectorStore()
record = chunk_record(atc_codes=["A10BA02", "A10BD20"])
loader(store).load([(record, vector())])
assert len(store.find_by_payload(COLLECTION, {"atc_codes": "A10BD20"})) == 1
assert len(store.find_by_payload(COLLECTION, {"atc_codes": "J05AF06"})) == 0
def test_a_filter_matching_nothing_returns_empty_rather_than_raising():
store = InMemoryVectorStore()
loader(store).load(pairs(2))
assert store.find_by_payload(COLLECTION, {"drug_id": "khong_ton_tai"}) == []
def test_a_filter_with_no_condition_is_refused():
store = InMemoryVectorStore()
loader(store).load(pairs(1))
with pytest.raises(ValueError, match="at least one condition"):
store.find_by_payload(COLLECTION, {})
def test_parts_reassemble_in_order_into_the_whole_section():
store = InMemoryVectorStore()
bodies = ["Phần một.", "Phần hai.", "Phần ba."]
records = [
(
chunk_record(
chunk_id=f"metformin__chong_chi_dinh__{i}",
drug_id="metformin",
section_key="chong_chi_dinh",
text=body,
part_index=i,
part_count=len(bodies),
),
vector(),
)
for i, body in enumerate(bodies)
]
loader(store).load(records)
found = store.find_by_payload(
COLLECTION, {"drug_id": "metformin", "section_key": "chong_chi_dinh"}
)
ordered = sorted(found, key=lambda p: p.payload["part_index"])
assert [p.payload["text"] for p in ordered] == bodies
assert {p.payload["part_count"] for p in found} == {3}
# --- corpus digest and reading -------------------------------------------
def test_corpus_sha256_changes_when_a_single_byte_changes(tmp_path):
path = tmp_path / "chunks.jsonl"
path.write_text(json.dumps(chunk_record()) + "\n", encoding="utf-8")
before = corpus_sha256(path)
path.write_text(
json.dumps(chunk_record(text="Người lớn: 301 mg.")) + "\n", encoding="utf-8"
)
assert corpus_sha256(path) != before
assert len(before) == 64
def test_the_same_data_hashes_the_same_under_crlf_and_lf(tmp_path):
"""A gate that cries wolf gets switched off.
A raw-byte digest made a Windows CRLF checkout and a Linux LF checkout of
identical data disagree, so A6 would refuse a CI load against the very
corpus it was built from.
"""
body = json.dumps(chunk_record()) + "\n" + json.dumps(chunk_record("b")) + "\n"
lf = tmp_path / "lf.jsonl"
crlf = tmp_path / "crlf.jsonl"
lf.write_bytes(body.encode("utf-8"))
crlf.write_bytes(body.replace("\n", "\r\n").encode("utf-8"))
assert corpus_sha256(lf) == corpus_sha256(crlf)
assert crlf.stat().st_size > lf.stat().st_size, "the files really do differ"
def test_blank_lines_are_skipped_and_bad_json_names_its_line(tmp_path):
path = tmp_path / "chunks.jsonl"
path.write_text(
json.dumps(chunk_record()) + "\n\n" + json.dumps(chunk_record("b")) + "\n",
encoding="utf-8",
)
assert count_chunks(path) == 2
broken = tmp_path / "broken.jsonl"
broken.write_text(json.dumps(chunk_record()) + "\n{oops\n", encoding="utf-8")
with pytest.raises(ValueError, match="line 2 is not valid JSON"):
list(iter_chunk_records(broken))
# --- the real artifact ----------------------------------------------------
@pytest.mark.skipif(
not REAL_CHUNKS.exists(), reason="chunks.jsonl has not been generated"
)
def test_every_real_chunk_satisfies_the_loader_provenance_contract():
"""Whole-artifact scope: all records in `data/processed/chunks.jsonl`."""
seen_ids = set()
seen_points = set()
total = 0
for record in iter_chunk_records(REAL_CHUNKS):
validate_chunk_record(record)
point = point_id_for(record["chunk_id"])
assert point not in seen_points, f"point id collision on {record['chunk_id']}"
seen_points.add(point)
seen_ids.add(record["chunk_id"])
total += 1
assert total == len(seen_ids), "duplicate chunk_id in the artifact"
assert total == len(seen_points)
# A floor, not the exact count: the corpus is regenerated as `segment/`
# changes, but a truncated or half-written artifact must not pass as
# whole-artifact evidence. Measured 15,066 records on 2026-08-04.
assert total > 10_000, f"chunks.jsonl looks truncated: only {total} records"