实现第一版内存清洗核心

落实不可变数据契约、组件基类、原子修改执行器与顺序流水线。补充稳定性复查、审计记录、测试和当前机制文档。
This commit is contained in:
2026-08-22 01:03:03 +08:00
parent 8ac4f1dd12
commit 3edeeaf30e
15 changed files with 2023 additions and 145 deletions
+199
View File
@@ -0,0 +1,199 @@
from __future__ import annotations
import pytest
from mdpolish import (
ComponentInfo,
DocumentSnapshot,
EditValidationError,
ProposedChange,
TextEdit,
TextSpan,
apply_component_batch,
)
COMPONENT = ComponentInfo(
component_id="test.component",
version="1.2.3",
parameters=(),
applicability="测试精确文本编辑,只处理测试字符串,排除其他输入。",
)
def make_edit(snapshot: DocumentSnapshot, start: int, end: int, replacement: str) -> TextEdit:
return TextEdit(
snapshot_sha256=snapshot.sha256,
span=TextSpan(start, end),
expected_text=snapshot.markdown[start:end],
replacement=replacement,
)
def make_proposal(snapshot: DocumentSnapshot, *edits: TextEdit, reason: str = "test reason") -> ProposedChange:
return ProposedChange(snapshot_sha256=snapshot.sha256, reason=reason, edits=edits)
def test_applies_insert_delete_and_replace() -> None:
insert_snapshot = DocumentSnapshot("ab")
delete_snapshot = DocumentSnapshot("abc")
replace_snapshot = DocumentSnapshot("abc")
inserted = apply_component_batch(
insert_snapshot,
(make_proposal(insert_snapshot, make_edit(insert_snapshot, 1, 1, "X")),),
COMPONENT,
0,
)
deleted = apply_component_batch(
delete_snapshot,
(make_proposal(delete_snapshot, make_edit(delete_snapshot, 1, 2, "")),),
COMPONENT,
0,
)
replaced = apply_component_batch(
replace_snapshot,
(make_proposal(replace_snapshot, make_edit(replace_snapshot, 1, 2, "X")),),
COMPONENT,
0,
)
assert inserted.snapshot.markdown == "aXb"
assert deleted.snapshot.markdown == "ac"
assert replaced.snapshot.markdown == "aXc"
def test_multiple_edits_apply_backwards_but_report_in_source_order() -> None:
snapshot = DocumentSnapshot("abcdef")
proposal = make_proposal(
snapshot,
make_edit(snapshot, 4, 6, "F"),
make_edit(snapshot, 0, 1, "A"),
reason="normalize two locations",
)
applied = apply_component_batch(snapshot, (proposal,), COMPONENT, 3)
assert applied.snapshot.markdown == "AbcdF"
assert [change.span.start for change in applied.changes] == [0, 4]
assert {change.before_sha256 for change in applied.changes} == {snapshot.sha256}
assert {change.after_sha256 for change in applied.changes} == {applied.snapshot.sha256}
assert {change.proposal_ref for change in applied.changes} == {
applied.changes[0].proposal_ref,
}
assert {change.reason for change in applied.changes} == {"normalize two locations"}
assert [change.edit_index for change in applied.changes] == [1, 0]
assert all(change.component_position == 3 for change in applied.changes)
def test_adjacent_nonempty_ranges_are_allowed() -> None:
snapshot = DocumentSnapshot("abcd")
proposal = make_proposal(
snapshot,
make_edit(snapshot, 0, 2, "A"),
make_edit(snapshot, 2, 4, "D"),
)
applied = apply_component_batch(snapshot, (proposal,), COMPONENT, 0)
assert applied.snapshot.markdown == "AD"
def test_overlapping_ranges_fail_without_changing_snapshot() -> None:
snapshot = DocumentSnapshot("abcdef")
proposal = make_proposal(
snapshot,
make_edit(snapshot, 1, 4, "X"),
make_edit(snapshot, 3, 5, "Y"),
)
with pytest.raises(EditValidationError, match="conflicting"):
apply_component_batch(snapshot, (proposal,), COMPONENT, 0)
assert snapshot.markdown == "abcdef"
assert snapshot.sha256 == DocumentSnapshot("abcdef").sha256
def test_duplicate_edits_fail_explicitly() -> None:
snapshot = DocumentSnapshot("abc")
edit = make_edit(snapshot, 0, 1, "A")
proposal = make_proposal(snapshot, edit, edit)
with pytest.raises(EditValidationError, match="duplicate"):
apply_component_batch(snapshot, (proposal,), COMPONENT, 0)
def test_distinct_insert_points_are_allowed() -> None:
snapshot = DocumentSnapshot("abcd")
proposal = make_proposal(
snapshot,
make_edit(snapshot, 1, 1, "X"),
make_edit(snapshot, 3, 3, "Y"),
)
applied = apply_component_batch(snapshot, (proposal,), COMPONENT, 0)
assert applied.snapshot.markdown == "aXbcYd"
def test_same_insert_point_conflicts() -> None:
snapshot = DocumentSnapshot("abc")
proposal = make_proposal(
snapshot,
make_edit(snapshot, 1, 1, "X"),
make_edit(snapshot, 1, 1, "Y"),
)
with pytest.raises(EditValidationError, match="conflicting"):
apply_component_batch(snapshot, (proposal,), COMPONENT, 0)
@pytest.mark.parametrize("insert_position", [1, 2, 3])
def test_insert_at_start_inside_or_end_of_nonempty_range_conflicts(insert_position: int) -> None:
snapshot = DocumentSnapshot("abcd")
proposal = make_proposal(
snapshot,
make_edit(snapshot, 1, 3, "X"),
make_edit(snapshot, insert_position, insert_position, "Y"),
)
with pytest.raises(EditValidationError, match="conflicting"):
apply_component_batch(snapshot, (proposal,), COMPONENT, 0)
def test_stale_hash_out_of_range_and_expected_text_mismatch_fail() -> None:
original = DocumentSnapshot("abc")
current = DocumentSnapshot("abd")
stale = make_proposal(original, make_edit(original, 0, 1, "A"))
with pytest.raises(EditValidationError, match="stale"):
apply_component_batch(current, (stale,), COMPONENT, 0)
out_of_range_edit = TextEdit(current.sha256, TextSpan(2, 5), "dxx", "D")
out_of_range = make_proposal(current, out_of_range_edit)
with pytest.raises(EditValidationError, match="outside"):
apply_component_batch(current, (out_of_range,), COMPONENT, 0)
mismatch_edit = TextEdit(current.sha256, TextSpan(0, 1), "z", "A")
mismatch = make_proposal(current, mismatch_edit)
with pytest.raises(EditValidationError, match="expected_text"):
apply_component_batch(current, (mismatch,), COMPONENT, 0)
def test_conflict_across_proposals_rejects_whole_component_batch() -> None:
snapshot = DocumentSnapshot("abcdef")
first = make_proposal(snapshot, make_edit(snapshot, 0, 3, "X"), reason="first")
second = make_proposal(snapshot, make_edit(snapshot, 2, 4, "Y"), reason="second")
with pytest.raises(EditValidationError, match="conflicting"):
apply_component_batch(snapshot, (first, second), COMPONENT, 0)
assert snapshot.markdown == "abcdef"
def test_empty_component_batch_keeps_same_snapshot_and_records_nothing() -> None:
snapshot = DocumentSnapshot("abc")
applied = apply_component_batch(snapshot, (), COMPONENT, 0)
assert applied.snapshot is snapshot
assert applied.changes == ()