|
18 | 18 | from cppmega_mlx.data.prompt_graph_index import ( |
19 | 19 | ClangPromptProjectIndexProducer, |
20 | 20 | ) |
| 21 | +from cppmega_mlx.data.prompt_graph_provenance import ( |
| 22 | + INDEX_INTEGRITY_VERSION, |
| 23 | + validate_shared_provenance, |
| 24 | +) |
21 | 25 | from cppmega_mlx.data.symbol_identity import compute_symbol_id |
22 | 26 |
|
23 | 27 |
|
@@ -687,3 +691,90 @@ def test_corrupt_producer_cache_fails_closed(tmp_path: Path) -> None: |
687 | 691 |
|
688 | 692 | with pytest.raises(ValueError, match="cached prompt project index"): |
689 | 693 | producer.build(FIXTURE, project_id="tests/case3-corrupt") |
| 694 | + |
| 695 | + |
| 696 | +# --------------------------------------------------------------------------- |
| 697 | +# CR-03: index_integrity_version provenance round-trip |
| 698 | +# --------------------------------------------------------------------------- |
| 699 | + |
| 700 | + |
| 701 | +def test_cr03_producer_emits_index_integrity_version(tmp_path: Path) -> None: |
| 702 | + """MLX producer must emit index_integrity_version in provenance output.""" |
| 703 | + project_id = "tests/case3-cr03-emit" |
| 704 | + result = ClangPromptProjectIndexProducer( |
| 705 | + cache_dir=tmp_path / "index-cache", |
| 706 | + indexer_root=ROOT, |
| 707 | + ).build(FIXTURE, project_id=project_id) |
| 708 | + |
| 709 | + assert result.receipt["index_integrity_version"] == INDEX_INTEGRITY_VERSION |
| 710 | + |
| 711 | + |
| 712 | +def test_cr03_provenance_round_trip_validates(tmp_path: Path) -> None: |
| 713 | + """Serialize MLX index, reload, and validate through shared provenance.""" |
| 714 | + project_id = "tests/case3-cr03-roundtrip" |
| 715 | + result = ClangPromptProjectIndexProducer( |
| 716 | + cache_dir=tmp_path / "index-cache", |
| 717 | + indexer_root=ROOT, |
| 718 | + ).build(FIXTURE, project_id=project_id) |
| 719 | + |
| 720 | + # Serialize and reload |
| 721 | + payload = result.index.to_dict() |
| 722 | + serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False) |
| 723 | + reloaded = PromptProjectIndex.from_dict(json.loads(serialized)) |
| 724 | + |
| 725 | + # Must pass shared provenance validation |
| 726 | + validate_shared_provenance(reloaded, expected_indexer_root=ROOT) |
| 727 | + |
| 728 | + |
| 729 | +def test_cr03_tampered_integrity_version_rejected(tmp_path: Path) -> None: |
| 730 | + """Deliberately changed index_integrity_version must be rejected.""" |
| 731 | + project_id = "tests/case3-cr03-tamper" |
| 732 | + result = ClangPromptProjectIndexProducer( |
| 733 | + cache_dir=tmp_path / "index-cache", |
| 734 | + indexer_root=ROOT, |
| 735 | + ).build(FIXTURE, project_id=project_id) |
| 736 | + |
| 737 | + payload = result.index.to_dict() |
| 738 | + payload["provenance"]["index_integrity_version"] = "999" |
| 739 | + tampered = PromptProjectIndex.from_dict(payload).with_integrity() |
| 740 | + |
| 741 | + with pytest.raises(ValueError, match="integrity version"): |
| 742 | + validate_shared_provenance(tampered, expected_indexer_root=ROOT) |
| 743 | + |
| 744 | + |
| 745 | +# --------------------------------------------------------------------------- |
| 746 | +# CR-07: foreign indexer_root rejection |
| 747 | +# --------------------------------------------------------------------------- |
| 748 | + |
| 749 | + |
| 750 | +def test_cr07_load_indexer_rejects_foreign_checkout(tmp_path: Path) -> None: |
| 751 | + """_load_indexer must reject an indexer_root outside the package checkout.""" |
| 752 | + foreign_root = tmp_path / "foreign_checkout" |
| 753 | + foreign_root.mkdir() |
| 754 | + (foreign_root / "tools" / "clang_indexer").mkdir(parents=True) |
| 755 | + (foreign_root / "tools" / "clang_indexer" / "index_project.py").write_text( |
| 756 | + "# foreign\n", encoding="utf-8" |
| 757 | + ) |
| 758 | + |
| 759 | + with pytest.raises(ValueError, match="same checkout"): |
| 760 | + prompt_graph_index_module._load_indexer(foreign_root) |
| 761 | + |
| 762 | + |
| 763 | +def test_cr07_producer_rejects_foreign_indexer_before_execution( |
| 764 | + tmp_path: Path, |
| 765 | +) -> None: |
| 766 | + """Producer.build must reject foreign indexer_root before module exec.""" |
| 767 | + foreign_root = tmp_path / "foreign_checkout" |
| 768 | + foreign_root.mkdir() |
| 769 | + (foreign_root / "tools" / "clang_indexer").mkdir(parents=True) |
| 770 | + (foreign_root / "tools" / "clang_indexer" / "index_project.py").write_text( |
| 771 | + "raise RuntimeError('must not execute')\n", encoding="utf-8" |
| 772 | + ) |
| 773 | + |
| 774 | + producer = ClangPromptProjectIndexProducer( |
| 775 | + cache_dir=tmp_path / "index-cache", |
| 776 | + indexer_root=foreign_root, |
| 777 | + ) |
| 778 | + |
| 779 | + with pytest.raises(ValueError, match="same checkout"): |
| 780 | + producer.build(FIXTURE, project_id="tests/case3-cr07-foreign") |
0 commit comments