from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from types import ModuleType
import pytest
SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "refresh-mutalyzer-fixtures.py"
def _load_refresh_module() -> ModuleType:
spec = importlib.util.spec_from_file_location("refresh_mutalyzer_fixtures", SCRIPT)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
refresh = _load_refresh_module()
def _upstream_cases() -> list[dict]:
return [
{
"input": "NM_000000.1:c.1A>T",
"to_test": True,
"keywords": ["upstream"],
"normalized": "NM_000000.1:c.1A>T",
"protein_description": "NP_000000.1:p.(Met1?)",
"input_spdi": "NM_000000.1:0:A:T",
},
{
"input": "NM_000000.1:c.9del",
"to_test": True,
"keywords": ["upstream"],
"normalized": "NM_000000.1:c.9del",
},
{
"input": "NM_000000.1:g.5del",
"to_test": True,
"keywords": ["upstream"],
"normalized": "NM_000000.1:n.5del",
},
]
def _existing_fixture() -> dict:
return {
"comparator_provenance": {"note": "pinned comparator"},
"clusters": [{"id": "protein", "title": "Protein consequence"}],
"cases": [
{
"input": "NM_000000.1:c.1A>T",
"to_test": True,
"keywords": ["upstream", "ferro: start-codon policy, see #857"],
"normalized": "NM_000000.1:c.1A>T",
"protein_description": "NP_000000.1:p.(Met1Leu)",
"spec_citation": "substitution.md:12",
"accepted_divergence": "upstream under-reports the start codon",
},
{
"input": "NM_000000.1:c.9del",
"to_test": True,
"keywords": ["upstream"],
"normalized": "NM_000000.1:c.9del",
},
{
"input": "NM_000000.1:g.5del",
"to_test": True,
"keywords": ["upstream"],
"removed_keys": ["normalized"],
"errors": ["ECOORDINATESYSTEMMISMATCH"],
},
{
"input": "NM_111111.1:c.100_102dup",
"to_test": True,
"keywords": [
f"{refresh.HAND_ADDED_PREFIX}protein",
"Derived from the pinned comparator.",
],
"protein_description": "NP_111111.1:p.(Ala34dup)",
},
],
}
def test_merge_preserves_hand_rows_dispositions_and_corrections() -> None:
payload = refresh.build_refresh_payload(_upstream_cases(), _existing_fixture(), force=True)
by_input = {c["input"]: c for c in payload["cases"]}
assert len(payload["cases"]) == 4
assert payload["clusters"] == [{"id": "protein", "title": "Protein consequence"}]
assert payload["comparator_provenance"] == {"note": "pinned comparator"}
hand = by_input["NM_111111.1:c.100_102dup"]
assert refresh._is_hand_added(hand)
assert hand["protein_description"] == "NP_111111.1:p.(Ala34dup)"
merged = by_input["NM_000000.1:c.1A>T"]
assert merged["spec_citation"] == "substitution.md:12"
assert merged["accepted_divergence"] == "upstream under-reports the start codon"
assert merged["protein_description"] == "NP_000000.1:p.(Met1Leu)"
assert merged["input_spdi"] == "NM_000000.1:0:A:T"
assert merged["keywords"] == ["upstream", "ferro: start-codon policy, see #857"]
assert by_input["NM_000000.1:c.9del"]["normalized"] == "NM_000000.1:c.9del"
removed = by_input["NM_000000.1:g.5del"]
assert "normalized" not in removed
assert removed["errors"] == ["ECOORDINATESYSTEMMISMATCH"]
assert removed["removed_keys"] == ["normalized"]
def test_duplicate_inputs_are_paired_by_occurrence_order() -> None:
upstream = [
{"input": "NG_000000.1:g.26_31del", "keywords": [], "normalized": "b"},
{"input": "NG_000000.1:g.26_31del", "keywords": [], "normalized": "a"},
]
existing = {
"cases": [
{
"input": "NG_000000.1:g.26_31del",
"keywords": [],
"normalized": "a",
"spec_citation": "first",
},
{
"input": "NG_000000.1:g.26_31del",
"keywords": [],
"normalized": "b",
"spec_citation": "second",
},
]
}
cases = refresh.build_refresh_payload(upstream, existing, force=True)["cases"]
assert [(c["normalized"], c["spec_citation"]) for c in cases] == [
("a", "first"),
("b", "second"),
]
def test_unmatched_curated_duplicate_tail_is_preserved() -> None:
upstream = [{"input": "NG_000000.1:g.26_31del", "keywords": [], "normalized": "a"}]
existing = {
"cases": [
{
"input": "NG_000000.1:g.26_31del",
"keywords": [],
"normalized": "a",
"spec_citation": "first",
},
{
"input": "NG_000000.1:g.26_31del",
"keywords": [],
"normalized": "b",
"spec_citation": "second",
},
]
}
cases = refresh.build_refresh_payload(upstream, existing, force=True)["cases"]
citations = sorted(c["spec_citation"] for c in cases)
assert citations == ["first", "second"]
def test_unmatched_curated_row_triggers_refusal() -> None:
upstream = [{"input": "NG_000000.1:g.26_31del", "keywords": [], "normalized": "a"}]
existing = {
"cases": [
{"input": "NG_000000.1:g.26_31del", "keywords": [], "normalized": "a"},
{"input": "NG_000000.1:g.26_31del", "keywords": [], "normalized": "b"},
]
}
with pytest.raises(SystemExit) as excinfo:
refresh.build_refresh_payload(upstream, existing, force=False)
message = str(excinfo.value)
assert "refusing to refresh" in message
assert "1 curated row(s) upstream no longer ships" in message
def test_hand_added_marker_is_found_in_any_keyword_position() -> None:
case = {"keywords": ["cluster:protein", f"{refresh.HAND_ADDED_PREFIX}protein"]}
assert refresh._is_hand_added(case)
assert not refresh._is_hand_added({"keywords": ["upstream"]})
assert not refresh._is_hand_added({})
def test_new_upstream_expected_value_field_survives_without_removal_marker() -> None:
upstream = [
{
"input": "NM_000000.1:c.1A>T",
"keywords": ["upstream"],
"normalized": "NM_000000.1:c.1A>T",
"infos": ["ISOMEINFO"],
}
]
existing = {
"cases": [
{
"input": "NM_000000.1:c.1A>T",
"keywords": ["upstream"],
"normalized": "NM_000000.1:c.1A>T",
"spec_citation": "sub.md:1",
}
]
}
merged = refresh.build_refresh_payload(upstream, existing, force=True)["cases"][0]
assert merged["infos"] == ["ISOMEINFO"]
assert merged["spec_citation"] == "sub.md:1"
def test_removed_keys_marker_drops_field_and_persists() -> None:
upstream = [
{
"input": "NM_000000.1:g.5del",
"keywords": ["upstream"],
"normalized": "NM_000000.1:n.5del",
}
]
existing = {
"cases": [
{
"input": "NM_000000.1:g.5del",
"keywords": ["upstream"],
"removed_keys": ["normalized"],
"errors": ["ECOORDINATESYSTEMMISMATCH"],
}
]
}
merged = refresh.build_refresh_payload(upstream, existing, force=True)["cases"][0]
assert "normalized" not in merged
assert merged["removed_keys"] == ["normalized"]
assert merged["errors"] == ["ECOORDINATESYSTEMMISMATCH"]
def test_removed_keys_marker_triggers_refusal_when_field_absent_upstream() -> None:
upstream = [{"input": "NM_000000.1:g.5del", "keywords": ["upstream"]}]
existing = {
"cases": [
{
"input": "NM_000000.1:g.5del",
"keywords": ["upstream"],
"removed_keys": ["normalized"],
}
]
}
forced = refresh.build_refresh_payload(upstream, existing, force=True)["cases"][0]
assert forced["removed_keys"] == ["normalized"]
assert "normalized" not in forced
with pytest.raises(SystemExit) as excinfo:
refresh.build_refresh_payload(upstream, existing, force=False)
message = str(excinfo.value)
assert "refusing to refresh" in message
assert "1 upstream row(s) with deliberately-removed expected values" in message
def test_refuses_when_curation_present_and_not_forced() -> None:
with pytest.raises(SystemExit) as excinfo:
refresh.build_refresh_payload(_upstream_cases(), _existing_fixture(), force=False)
message = str(excinfo.value)
assert "refusing to refresh" in message
assert "1 hand-added case(s)" in message
assert "1 upstream row(s) carrying per-case dispositions" in message
assert "2 upstream row(s) whose expected values were hand-corrected" in message
assert "1 upstream row(s) with deliberately-removed expected values" in message
def test_fresh_import_against_empty_fixture_proceeds() -> None:
payload = refresh.build_refresh_payload(_upstream_cases(), {}, force=False)
assert [c["input"] for c in payload["cases"]] == [
"NM_000000.1:c.1A>T",
"NM_000000.1:c.9del",
"NM_000000.1:g.5del",
]
assert "clusters" not in payload
assert "comparator_provenance" not in payload
assert payload["source_commit"] == refresh.MUTALYZER_SHA
def test_corrupt_existing_fixture_raises_with_context(tmp_path: Path) -> None:
corrupt = tmp_path / "cases.json"
corrupt.write_text('{"cases": [')
with pytest.raises(SystemExit) as excinfo:
refresh._load_existing(corrupt)
message = str(excinfo.value)
assert str(corrupt) in message
assert "--force" in message