tests/test_diff.py
2.6 KB · 80 lines · python Raw
1 """Tests for MBOM diffing."""
2
3 from __future__ import annotations
4
5 import copy
6
7 from pqc_mbom import (
8 ComponentType,
9 MBOM,
10 MBOMBuilder,
11 ModelComponent,
12 diff_mboms,
13 )
14
15
16 def _clone_mbom(mbom: MBOM) -> MBOM:
17 return MBOM.from_json(mbom.to_json())
18
19
20 def test_added_component_detected(sample_mbom: MBOM) -> None:
21 new_mbom = _clone_mbom(sample_mbom)
22 new_mbom.components.append(ModelComponent(
23 component_id="eval-mmlu",
24 component_type=ComponentType.EVALUATION_BENCHMARK,
25 name="mmlu",
26 content_hash="e" * 64,
27 ))
28 new_mbom.recompute_root()
29 diff = diff_mboms(sample_mbom, new_mbom)
30 assert len(diff.added) == 1
31 assert diff.added[0].name == "mmlu"
32 assert diff.removed == []
33 assert diff.changed == []
34
35
36 def test_removed_component_detected(sample_mbom: MBOM) -> None:
37 new_mbom = _clone_mbom(sample_mbom)
38 new_mbom.components = [c for c in new_mbom.components if c.component_type != ComponentType.TOKENIZER]
39 new_mbom.recompute_root()
40 diff = diff_mboms(sample_mbom, new_mbom)
41 assert len(diff.removed) == 1
42 assert diff.removed[0].component_type == ComponentType.TOKENIZER
43 assert diff.added == []
44 assert diff.changed == []
45
46
47 def test_changed_component_detected(sample_mbom: MBOM) -> None:
48 new_mbom = _clone_mbom(sample_mbom)
49 # Same component_id but mutated hash - classic dataset-swap scenario.
50 target = next(c for c in new_mbom.components if c.component_type == ComponentType.TRAINING_DATA)
51 target.content_hash = "ffff" * 16
52 new_mbom.recompute_root()
53 diff = diff_mboms(sample_mbom, new_mbom)
54 assert len(diff.changed) == 1
55 old_c, new_c = diff.changed[0]
56 assert old_c.component_id == new_c.component_id
57 assert old_c.content_hash != new_c.content_hash
58
59
60 def test_no_changes_returns_empty_diff(sample_mbom: MBOM) -> None:
61 copy_mbom = _clone_mbom(sample_mbom)
62 diff = diff_mboms(sample_mbom, copy_mbom)
63 assert diff.added == []
64 assert diff.removed == []
65 assert diff.changed == []
66 assert diff.is_empty
67
68
69 def test_builder_produces_diffable_mboms() -> None:
70 # Extra sanity: two independently built MBOMs with same content still
71 # produce a meaningful diff (their component_ids differ because of uuid,
72 # so they look like full add+remove).
73 b1 = MBOMBuilder("M", "1").add_weights("w", content_hash="a" * 64)
74 b2 = MBOMBuilder("M", "1").add_weights("w", content_hash="a" * 64)
75 d = diff_mboms(b1.build(), b2.build())
76 # All ids differ -> everything is add+remove
77 assert len(d.added) == 1 and len(d.removed) == 1
78 assert not d.is_empty
79 _ = copy # silence unused-import lint
80