from __future__ import annotations from pathlib import Path from gcmd_classifier.classification import ( ClassificationCandidate, remove_redundant_classifications, validate_candidate, ) from gcmd_classifier.models import ClassificationRecord, SupportType from gcmd_classifier.vocabulary import load_vocabulary FIXTURE_PATH = Path("tests/fixtures/gcmd_hierarchy_small.json") def _index(): return load_vocabulary(FIXTURE_PATH) def _candidate(uuid: str, *, branch_id: str | None) -> ClassificationCandidate: index = _index() record = index.get(uuid) topic = record if record.level == "Topic" else index.get(index.ancestors_of(uuid)[-1]) term = None if record.level == "Term": term = record elif record.level.startswith("Variable_Level_"): term = next( index.get(ancestor) for ancestor in index.ancestors_of(uuid) if index.get(ancestor).level == "Term" ) return ClassificationCandidate( final_uuid=record.UUID, final_name=record.name, final_level=record.level, final_canonical_path=record.canonical_path, path_components=record.path_components, topic_uuid=topic.UUID, topic_name=topic.name, term_uuid=None if term is None else term.UUID, term_name=None if term is None else term.name, branch_id=branch_id, evidence=f"Evidence for {record.name}.", support_type=SupportType.EXPLICIT, confidence=0.7, ) def _record(uuid: str, *, branch_id: str | None) -> ClassificationRecord: result = validate_candidate(_candidate(uuid, branch_id=branch_id), _index()) assert result.classification is not None return result.classification def _warning_codes(result) -> list[str]: return [warning.code for warning in result.warnings] def test_exact_duplicate_uuid_removed() -> None: first = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") duplicate = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a-copy") result = remove_redundant_classifications([first, duplicate], _index()) assert [record.UUID for record in result.classifications] == ["vl2-carbon-dioxide"] assert [record.UUID for record in result.removed] == ["vl2-carbon-dioxide"] assert "DUPLICATE_UUID_REMOVED" in _warning_codes(result) def test_exact_duplicate_canonical_path_removed() -> None: first = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") duplicate_path = _record("vl2-methane", branch_id="topic:t/term:x/variable:b").model_copy( update={"canonical_path": first.canonical_path} ) result = remove_redundant_classifications([first, duplicate_path], _index()) assert [record.UUID for record in result.classifications] == ["vl2-carbon-dioxide"] assert [record.UUID for record in result.removed] == ["vl2-methane"] assert "DUPLICATE_CANONICAL_PATH_REMOVED" in _warning_codes(result) def test_ancestor_removed_when_descendant_exists_in_same_branch_lineage() -> None: ancestor = _record("term-atmospheric-chemistry", branch_id="topic:t/term:x") descendant = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") result = remove_redundant_classifications([ancestor, descendant], _index()) assert [record.UUID for record in result.classifications] == ["vl2-carbon-dioxide"] assert [record.UUID for record in result.removed] == ["term-atmospheric-chemistry"] assert "ANCESTOR_REMOVED_SAME_BRANCH" in _warning_codes(result) def test_deepest_descendant_retained() -> None: term = _record("term-atmospheric-chemistry", branch_id="topic:t/term:x") variable_level_1 = _record("vl1-atmosphere-carbon", branch_id="topic:t/term:x/variable:a") variable_level_3 = _record( "vl3-carbon-dioxide-profiles", branch_id="topic:t/term:x/variable:a/variable:b/variable:c", ) result = remove_redundant_classifications([term, variable_level_1, variable_level_3], _index()) assert [record.UUID for record in result.classifications] == ["vl3-carbon-dioxide-profiles"] assert {record.UUID for record in result.removed} == { "term-atmospheric-chemistry", "vl1-atmosphere-carbon", } def test_ancestor_preserved_when_independent_branch() -> None: ancestor = _record("term-atmospheric-chemistry", branch_id="topic:t/term:independent") descendant = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") result = remove_redundant_classifications([ancestor, descendant], _index()) assert [record.UUID for record in result.classifications] == [ "term-atmospheric-chemistry", "vl2-carbon-dioxide", ] assert result.removed == () assert "ANCESTOR_PRESERVED_INDEPENDENT_BRANCH" in _warning_codes(result) def test_sibling_classifications_preserved() -> None: dioxide = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") methane = _record("vl2-methane", branch_id="topic:t/term:x/variable:b") result = remove_redundant_classifications([dioxide, methane], _index()) assert [record.UUID for record in result.classifications] == [ "vl2-carbon-dioxide", "vl2-methane", ] assert result.removed == () def test_same_concept_name_in_different_branches_is_preserved_when_uuid_and_path_differ() -> None: atmosphere_carbon = _record("vl1-atmosphere-carbon", branch_id="topic:a/term:a/variable:a") ocean_carbon = _record("vl1-ocean-carbon", branch_id="topic:o/term:o/variable:o") result = remove_redundant_classifications([atmosphere_carbon, ocean_carbon], _index()) assert [record.name for record in result.classifications] == ["CARBON", "CARBON"] assert [record.UUID for record in result.classifications] == [ "vl1-atmosphere-carbon", "vl1-ocean-carbon", ] assert result.removed == () def test_redundancy_uses_uuid_ancestry_not_string_prefix_matching() -> None: atmosphere_carbon = _record("vl1-atmosphere-carbon", branch_id="topic:a/term:a/variable:a") ocean_carbon = _record("vl1-ocean-carbon", branch_id="topic:o/term:o/variable:o") assert atmosphere_carbon.name == ocean_carbon.name result = remove_redundant_classifications([atmosphere_carbon, ocean_carbon], _index()) assert result.retained_count == 2 assert result.removed_count == 0 def test_insufficient_branch_provenance_preserves_ancestor_with_warning() -> None: ancestor = _record("term-atmospheric-chemistry", branch_id=None) descendant = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") result = remove_redundant_classifications([ancestor, descendant], _index()) assert [record.UUID for record in result.classifications] == [ "term-atmospheric-chemistry", "vl2-carbon-dioxide", ] assert "ANCESTOR_PRESERVED_INSUFFICIENT_PROVENANCE" in _warning_codes(result) def test_removal_and_preservation_diagnostics_include_branch_provenance() -> None: ancestor = _record("term-atmospheric-chemistry", branch_id="topic:t/term:x") descendant = _record("vl2-carbon-dioxide", branch_id="topic:t/term:x/variable:a") result = remove_redundant_classifications([ancestor, descendant], _index()) removal_warning = next( warning for warning in result.warnings if warning.code == "ANCESTOR_REMOVED_SAME_BRANCH" ) assert removal_warning.details["branch_id"] == "topic:t/term:x" assert removal_warning.details["descendant_branch_id"] == "topic:t/term:x/variable:a"