from __future__ import annotations from dataclasses import FrozenInstanceError from typing import cast import pytest from mdpolish import DocumentSnapshot, Modifier, ModifierContractError, ProposedChange def no_changes(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]: return () def test_plain_function_and_mapping_parameters_create_immutable_modifier() -> None: source_parameters: dict[str, object] = { "enabled": True, "nested": {"count": 2}, "values": ["a", "b"], } modifier = Modifier( modifier_id="example.rule", version="1.2.3", parameters=source_parameters, applicability="处理虚构测试输入;排除其他文本。", propose=no_changes, ) source_parameters["enabled"] = False assert modifier.propose is no_changes assert modifier.parameters == ( ("enabled", True), ("nested", (("count", 2),)), ("values", ("a", "b")), ) with pytest.raises(FrozenInstanceError): modifier.version = "2.0.0" # type: ignore[misc] def test_normalized_parameters_are_sorted_and_validated() -> None: modifier = Modifier( modifier_id="example.normalized", version="1.0.0", parameters=(("z", 1), ("a", ("x", 2))), applicability="处理虚构测试输入。", propose=no_changes, ) assert modifier.parameters == (("a", ("x", 2)), ("z", 1)) @pytest.mark.parametrize("modifier_id", ["", "Upper", "has space", ".leading", "trailing."]) def test_invalid_modifier_id_is_rejected(modifier_id: str) -> None: with pytest.raises(ModifierContractError, match="modifier_id"): Modifier( modifier_id=modifier_id, version="1.0.0", parameters=(), applicability="测试。", propose=no_changes, ) @pytest.mark.parametrize("version", ["", "1", "1.0", "v1.0.0", "01.0.0", "1.0.0-beta"]) def test_invalid_version_is_rejected(version: str) -> None: with pytest.raises(ModifierContractError, match="version"): Modifier( modifier_id="example.rule", version=version, parameters=(), applicability="测试。", propose=no_changes, ) @pytest.mark.parametrize( "parameters", [ {"bad": {1}}, {"bad": float("inf")}, (("duplicate", 1), ("duplicate", 2)), (("bad", object()),), ("not-a-pair",), ], ) def test_invalid_parameters_are_rejected(parameters: object) -> None: with pytest.raises(ModifierContractError): Modifier( modifier_id="example.rule", version="1.0.0", parameters=parameters, # type: ignore[arg-type] applicability="测试。", propose=no_changes, ) def test_empty_applicability_and_non_callable_proposal_are_rejected() -> None: with pytest.raises(ModifierContractError, match="applicability"): Modifier( modifier_id="example.rule", version="1.0.0", parameters=(), applicability=" ", propose=no_changes, ) with pytest.raises(ModifierContractError, match="callable"): Modifier( modifier_id="example.rule", version="1.0.0", parameters=(), applicability="测试。", propose=cast(object, None), # type: ignore[arg-type] )