Files
mdpolish/tests/test_component.py
T
Bepr4 3edeeaf30e 实现第一版内存清洗核心
落实不可变数据契约、组件基类、原子修改执行器与顺序流水线。补充稳定性复查、审计记录、测试和当前机制文档。
2026-08-22 01:03:03 +08:00

122 lines
4.0 KiB
Python

from __future__ import annotations
from collections.abc import Mapping
from typing import cast
import pytest
from mdpolish import Component, ComponentContractError, DocumentSnapshot, ProposedChange, TextEdit, TextSpan
class ExampleComponent(Component):
def __init__(
self,
*,
component_id: object = "test.example",
version: object = "1.2.3",
parameters: object = None,
applicability: object = "处理测试标记,要求精确匹配,排除所有其他内容。",
) -> None:
self._component_id = component_id
self._version = version
self._parameters = {} if parameters is None else parameters
self._applicability = applicability
@property
def component_id(self) -> str:
return cast(str, self._component_id)
@property
def version(self) -> str:
return cast(str, self._version)
@property
def parameters(self) -> Mapping[str, object]:
return cast(Mapping[str, object], self._parameters)
@property
def applicability(self) -> str:
return cast(str, self._applicability)
def _propose_changes(self, snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
if not snapshot.markdown:
return ()
edit = TextEdit(snapshot.sha256, TextSpan(0, 1), snapshot.markdown[0], "X")
return (ProposedChange(snapshot.sha256, "replace first character", (edit,)),)
class ListReturningComponent(ExampleComponent):
def _propose_changes(self, snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
return cast(tuple[ProposedChange, ...], [])
class WrongValueComponent(ExampleComponent):
def _propose_changes(self, snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
return cast(tuple[ProposedChange, ...], ("wrong",))
def test_component_metadata_is_validated_and_parameters_are_frozen_deterministically() -> None:
component = ExampleComponent(
parameters={
"z": [1, {"b": False, "a": None}],
"a": "value",
}
)
info = component._component_info()
assert info.component_id == "test.example"
assert info.version == "1.2.3"
assert info.parameters == (
("a", "value"),
("z", (1, (("a", None), ("b", False)))),
)
@pytest.mark.parametrize("component_id", ["", "Uppercase", "has space", "two..dots", "_leading"])
def test_invalid_component_id_is_a_contract_error(component_id: str) -> None:
with pytest.raises(ComponentContractError, match="component_id"):
ExampleComponent(component_id=component_id)._component_info()
@pytest.mark.parametrize("version", ["1", "1.2", "v1.2.3", "01.2.3", "1.2.3-alpha"])
def test_invalid_version_is_a_contract_error(version: str) -> None:
with pytest.raises(ComponentContractError, match=r"MAJOR.MINOR.PATCH"):
ExampleComponent(version=version)._component_info()
def test_empty_applicability_is_a_contract_error() -> None:
with pytest.raises(ComponentContractError, match="applicability"):
ExampleComponent(applicability=" \n")._component_info()
@pytest.mark.parametrize(
"parameters",
[
{"bad": {1, 2}},
{"bad": float("inf")},
{"bad": float("nan")},
{1: "non-string key"},
["not", "a", "mapping"],
],
)
def test_unrepresentable_parameters_are_contract_errors(parameters: object) -> None:
with pytest.raises(ComponentContractError, match="parameter"):
ExampleComponent(parameters=parameters)._component_info()
def test_collect_proposals_requires_a_tuple_of_proposed_changes() -> None:
snapshot = DocumentSnapshot("abc")
with pytest.raises(ComponentContractError, match="return a tuple"):
ListReturningComponent()._collect_proposals(snapshot)
with pytest.raises(ComponentContractError, match="only ProposedChange"):
WrongValueComponent()._collect_proposals(snapshot)
def test_component_exposes_no_public_check_or_transform_shortcut() -> None:
component = ExampleComponent()
assert not hasattr(component, "check")
assert not hasattr(component, "transform")