from __future__ import annotations import re import pytest from mdpolish import Modifier, ModifierContractError, Pipeline, RunStatus, regex_replace def test_factory_returns_modifier_and_expands_capture_groups() -> None: modifier = regex_replace( modifier_id="example.swap-date", version="1.0.0", pattern=r"(\d{4})-(\d{2})-(\d{2})", replacement=r"\3/\2/\1", applicability="处理 ISO 形状的虚构日期;不验证真实日历日期。", ) result = Pipeline([modifier]).transform("A 2026-08-26, B 2027-01-02") assert isinstance(modifier, Modifier) assert result.status is RunStatus.SUCCESS assert result.output_markdown == "A 26/08/2026, B 02/01/2027" assert len(result.changes) == 2 assert result.changes[0].before == "2026-08-26" assert result.changes[0].after == "26/08/2026" def test_flags_and_factory_parameters_are_recorded() -> None: modifier = regex_replace( modifier_id="example.case", version="2.0.0", pattern="token", replacement="value", flags=re.IGNORECASE | re.MULTILINE, ) result = Pipeline([modifier]).transform("TOKEN") assert result.status is RunStatus.SUCCESS assert result.modifiers[0].modifier_id == modifier.modifier_id assert result.modifiers[0].parameters == modifier.parameters assert dict(modifier.parameters) == { "flags": int(re.IGNORECASE | re.MULTILINE), "pattern": "token", "replacement": "value", } def test_zero_match_and_no_op_replacement_are_stable() -> None: missing = Pipeline( [regex_replace(modifier_id="example.missing", version="1.0.0", pattern="missing", replacement="new")] ).transform("text") no_op = Pipeline( [regex_replace(modifier_id="example.no-op", version="1.0.0", pattern="text", replacement="text")] ).transform("text") assert missing.output_markdown == "text" assert missing.changes == () assert no_op.status is RunStatus.SUCCESS assert no_op.changes == () @pytest.mark.parametrize("pattern", [r"", r"^", r"a*", r"(?=a)"]) def test_zero_length_matches_are_rejected(pattern: str) -> None: if pattern == r"(?=a)": modifier = regex_replace( modifier_id="example.zero", version="1.0.0", pattern=pattern, replacement="x", ) result = Pipeline([modifier]).transform("a") assert result.status is RunStatus.FAILED assert result.partial_markdown == "a" assert result.changes == () return with pytest.raises(ModifierContractError, match="zero-length"): regex_replace( modifier_id="example.zero", version="1.0.0", pattern=pattern, replacement="x", ) def test_invalid_pattern_flags_and_input_types_are_rejected() -> None: with pytest.raises(ModifierContractError, match="compile"): regex_replace(modifier_id="example.invalid", version="1.0.0", pattern="(", replacement="x") with pytest.raises(ModifierContractError, match="flags"): regex_replace( modifier_id="example.flags", version="1.0.0", pattern="x", replacement="y", flags=True, ) def test_successful_output_is_idempotent() -> None: pipeline = Pipeline( [regex_replace(modifier_id="example.space", version="1.0.0", pattern=r" {2,}", replacement=" ")] ) first = pipeline.transform("a b") assert first.output_markdown == "a b" assert first.output_markdown is not None assert pipeline.transform(first.output_markdown).changes == ()