feat(evolution): patch.py — 补丁引擎移植(算法 #9),51 测试全通过

从 TRM4 core/harness/patch.py (427 行) 零修改移植到 core/evolution/patch.py。

包含:
- 7 个常量(APPENDIX/MOMENTUM marker + MAX_CHARS + HEADING)
- appendix 区:追加/提取/替换/边界检测,损坏态 ValueError
- momentum 区:替换/提取/边界检测,注入防护
- apply_patch_with_report:4 种 op(append/insert_after/replace/delete)
- 冻结区坐标判定(每条 edit 重算)、report 1-based index
- 51 个单元测试覆盖全部公共 API 及边缘场景

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 09:44:05 -04:00
parent 4eefbfdd74
commit 3005f24577
2 changed files with 818 additions and 0 deletions
+391
View File
@@ -0,0 +1,391 @@
"""patch.py 补丁引擎单元测试。
覆盖四大区域:
1. TestRegionBounds — appendix/momentum 边界定位与损坏态检测
2. TestAppendix — 追加/提取/替换 appendix 区
3. TestMomentum — 替换/提取 momentum 区
4. TestApplyPatch — apply_patch_with_report 的 4 种 op + 冻结区 + 异常
"""
from __future__ import annotations
import pytest
from core.evolution.patch import (
APPENDIX_END,
APPENDIX_MAX_CHARS,
APPENDIX_START,
MOMENTUM_END,
MOMENTUM_HEADING,
MOMENTUM_MAX_CHARS,
MOMENTUM_START,
append_to_appendix,
appendix_region_bounds,
apply_patch_with_report,
extract_appendix_notes,
momentum_inner,
momentum_region_bounds,
replace_appendix_notes,
replace_momentum,
)
# ── TestRegionBounds ──────────────────────────────────────────────
class TestRegionBounds:
"""appendix_region_bounds / momentum_region_bounds 边界与损坏态检测。"""
# -- appendix --
def test_appendix_both_absent_returns_none(self) -> None:
assert appendix_region_bounds("no markers here") is None
def test_appendix_normal_pair(self) -> None:
text = f"head\n{APPENDIX_START}\nnotes\n{APPENDIX_END}\ntail"
start, end = appendix_region_bounds(text) # type: ignore[misc]
assert text[start:end].startswith(APPENDIX_START)
assert text[start:end].endswith(APPENDIX_END)
def test_appendix_only_start_raises(self) -> None:
with pytest.raises(ValueError, match="不配对"):
appendix_region_bounds(f"head\n{APPENDIX_START}\nno end")
def test_appendix_only_end_raises(self) -> None:
with pytest.raises(ValueError, match="不配对"):
appendix_region_bounds(f"head\n{APPENDIX_END}\nno start")
def test_appendix_repeated_start_raises(self) -> None:
text = f"{APPENDIX_START}\n{APPENDIX_START}\n{APPENDIX_END}"
with pytest.raises(ValueError, match="不配对"):
appendix_region_bounds(text)
def test_appendix_reversed_raises(self) -> None:
text = f"{APPENDIX_END}\nbody\n{APPENDIX_START}"
with pytest.raises(ValueError, match="不配对"):
appendix_region_bounds(text)
# -- momentum --
def test_momentum_both_absent_returns_none(self) -> None:
assert momentum_region_bounds("no markers here") is None
def test_momentum_normal_pair(self) -> None:
text = f"head\n{MOMENTUM_START}\nguidance\n{MOMENTUM_END}\ntail"
start, end = momentum_region_bounds(text) # type: ignore[misc]
assert text[start:end].startswith(MOMENTUM_START)
assert text[start:end].endswith(MOMENTUM_END)
def test_momentum_only_start_raises(self) -> None:
with pytest.raises(ValueError, match="不配对"):
momentum_region_bounds(f"head\n{MOMENTUM_START}\nno end")
def test_momentum_only_end_raises(self) -> None:
with pytest.raises(ValueError, match="不配对"):
momentum_region_bounds(f"head\n{MOMENTUM_END}\nno start")
def test_momentum_repeated_end_raises(self) -> None:
text = f"{MOMENTUM_START}\n{MOMENTUM_END}\n{MOMENTUM_END}"
with pytest.raises(ValueError, match="不配对"):
momentum_region_bounds(text)
def test_momentum_reversed_raises(self) -> None:
text = f"{MOMENTUM_END}\nbody\n{MOMENTUM_START}"
with pytest.raises(ValueError, match="不配对"):
momentum_region_bounds(text)
# ── TestAppendix ──────────────────────────────────────────────────
class TestAppendix:
"""append_to_appendix / extract_appendix_notes / replace_appendix_notes。"""
def test_append_creates_region(self) -> None:
out = append_to_appendix("# Skill\nbody", ["reminder A"])
assert APPENDIX_START in out
assert APPENDIX_END in out
assert "- reminder A" in out
def test_append_accumulates(self) -> None:
step1 = append_to_appendix("# Skill\nbody", ["note 1"])
step2 = append_to_appendix(step1, ["note 2"])
assert "- note 1" in step2
assert "- note 2" in step2
def test_append_empty_notes_returns_unchanged(self) -> None:
original = "# Skill\nbody"
assert append_to_appendix(original, []) is original
def test_append_all_whitespace_notes_returns_unchanged(self) -> None:
original = "# Skill\nbody"
assert append_to_appendix(original, [" ", "\t", ""]) == original
def test_extract_notes_roundtrip(self) -> None:
content = append_to_appendix("# Skill\nbody", ["aaa", "bbb"])
notes = extract_appendix_notes(content)
assert notes == ["aaa", "bbb"]
def test_extract_notes_no_region(self) -> None:
assert extract_appendix_notes("no region") == []
def test_replace_notes_overwrites(self) -> None:
content = append_to_appendix("# Skill\nbody", ["old"])
replaced = replace_appendix_notes(content, ["new1", "new2"])
notes = extract_appendix_notes(replaced)
assert notes == ["new1", "new2"]
assert "old" not in replaced
def test_replace_notes_empty_deletes_region(self) -> None:
content = append_to_appendix("# Skill\nbody", ["old"])
replaced = replace_appendix_notes(content, [])
assert APPENDIX_START not in replaced
assert APPENDIX_END not in replaced
def test_replace_notes_no_region_creates(self) -> None:
replaced = replace_appendix_notes("# Skill\nbody", ["fresh"])
assert "- fresh" in replaced
assert APPENDIX_START in replaced
def test_replace_notes_no_region_empty_notes_unchanged(self) -> None:
original = "# Skill\nbody"
assert replace_appendix_notes(original, []) == original
def test_append_warns_on_exceeding_max_chars(self, caplog: pytest.LogCaptureFixture) -> None:
"""appendix 区超长时 loguru warning,不截断。"""
long_note = "x" * (APPENDIX_MAX_CHARS + 100)
with caplog.at_level("WARNING"):
out = append_to_appendix("body", [long_note])
assert long_note in out # 不截断
# ── TestMomentum ──────────────────────────────────────────────────
class TestMomentum:
"""replace_momentum / momentum_inner。"""
def test_replace_creates_region(self) -> None:
out = replace_momentum("# Skill\nbody", "focus on X")
assert MOMENTUM_START in out
assert MOMENTUM_END in out
assert "focus on X" in out
def test_replace_overwrites(self) -> None:
step1 = replace_momentum("# Skill\nbody", "old guidance")
step2 = replace_momentum(step1, "new guidance")
assert "new guidance" in step2
assert "old guidance" not in step2
def test_replace_empty_guidance_clears(self) -> None:
"""空 guidance 合法:清空旧动量、保留标题和 marker。"""
step1 = replace_momentum("# Skill\nbody", "old guidance")
step2 = replace_momentum(step1, "")
assert MOMENTUM_START in step2
assert MOMENTUM_END in step2
assert "old guidance" not in step2
assert MOMENTUM_HEADING in step2
def test_replace_rejects_marker_in_guidance(self) -> None:
with pytest.raises(ValueError, match="marker"):
replace_momentum("body", f"bad {MOMENTUM_START} injection")
def test_replace_rejects_end_marker_in_guidance(self) -> None:
with pytest.raises(ValueError, match="marker"):
replace_momentum("body", f"bad {MOMENTUM_END} injection")
def test_momentum_inner_returns_guidance(self) -> None:
content = replace_momentum("# Skill\nbody", "focus on X")
inner = momentum_inner(content)
assert inner == "focus on X"
def test_momentum_inner_no_region(self) -> None:
assert momentum_inner("no region") == ""
def test_momentum_inner_strips_heading(self) -> None:
content = replace_momentum("body", "some guidance")
inner = momentum_inner(content)
assert MOMENTUM_HEADING not in inner
assert inner == "some guidance"
def test_replace_warns_on_exceeding_max_chars(self, caplog: pytest.LogCaptureFixture) -> None:
"""momentum 区超长时 loguru warning,不截断。"""
long_guidance = "y" * (MOMENTUM_MAX_CHARS + 100)
with caplog.at_level("WARNING"):
out = replace_momentum("body", long_guidance)
assert long_guidance in out # 不截断
def test_coexists_with_appendix(self) -> None:
"""momentum 与 appendix 独立共存。"""
content = append_to_appendix("# Skill\nbody", ["note A"])
content = replace_momentum(content, "focus on X")
assert APPENDIX_START in content
assert APPENDIX_END in content
assert MOMENTUM_START in content
assert MOMENTUM_END in content
notes = extract_appendix_notes(content)
assert notes == ["note A"]
inner = momentum_inner(content)
assert inner == "focus on X"
# ── TestApplyPatch ────────────────────────────────────────────────
class TestApplyPatch:
"""apply_patch_with_report 的 4 种 op + 冻结区 + 异常处理。"""
def test_append(self) -> None:
content = "# Title\n\nbody text"
edits = [{"op": "append", "target": "", "content": "new section"}]
out, reports = apply_patch_with_report(content, edits)
assert "new section" in out
assert reports[0]["status"] == "applied_append"
assert reports[0]["index"] == 1
def test_insert_after_success(self) -> None:
content = "# Title\n\nanchor line\n\nrest"
edits = [{"op": "insert_after", "target": "anchor line", "content": "inserted"}]
out, reports = apply_patch_with_report(content, edits)
assert "inserted" in out
assert reports[0]["status"] == "applied_insert_after"
def test_insert_after_missing_target_fallback(self) -> None:
content = "# Title\n\nbody"
edits = [{"op": "insert_after", "target": "nonexistent", "content": "payload"}]
out, reports = apply_patch_with_report(content, edits)
assert "payload" in out
assert reports[0]["status"] == "applied_insert_after_fallback"
def test_insert_after_protected_skip(self) -> None:
content = "# Title\n\nFROZEN BLOCK\n\nrest"
edits = [{"op": "insert_after", "target": "FROZEN BLOCK", "content": "nope"}]
out, reports = apply_patch_with_report(
content, edits, protected_spans=["FROZEN BLOCK"]
)
assert out == content
assert reports[0]["status"] == "skipped_protected"
def test_replace(self) -> None:
content = "# Title\n\nold text\n\nrest"
edits = [{"op": "replace", "target": "old text", "content": "new text"}]
out, reports = apply_patch_with_report(content, edits)
assert "new text" in out
assert "old text" not in out
assert reports[0]["status"] == "applied_replace"
def test_replace_protected_skip(self) -> None:
content = "# Title\n\nprotected\n\nrest"
edits = [{"op": "replace", "target": "protected", "content": "nope"}]
out, reports = apply_patch_with_report(
content, edits, protected_spans=["protected"]
)
assert "protected" in out
assert reports[0]["status"] == "skipped_protected"
def test_delete(self) -> None:
content = "# Title\n\nremove me\n\nrest"
edits = [{"op": "delete", "target": "remove me", "content": ""}]
out, reports = apply_patch_with_report(content, edits)
assert "remove me" not in out
assert reports[0]["status"] == "applied_delete"
def test_unknown_op(self) -> None:
edits = [{"op": "magic", "target": "x", "content": "y"}]
out, reports = apply_patch_with_report("body", edits)
assert reports[0]["status"] == "skipped_unknown_op"
def test_non_dict_edit(self) -> None:
edits = ["not a dict"] # type: ignore[list-item]
out, reports = apply_patch_with_report("body", edits)
assert reports[0]["status"] == "error"
assert "非 dict" in reports[0].get("error", "")
def test_missing_target_for_replace(self) -> None:
edits = [{"op": "replace", "target": "", "content": "payload"}]
out, reports = apply_patch_with_report("body", edits)
assert reports[0]["status"] == "skipped_missing_target"
def test_missing_target_for_delete(self) -> None:
edits = [{"op": "delete", "target": "", "content": ""}]
out, reports = apply_patch_with_report("body", edits)
assert reports[0]["status"] == "skipped_missing_target"
def test_report_index_is_1_based(self) -> None:
edits = [
{"op": "append", "target": "", "content": "a"},
{"op": "append", "target": "", "content": "b"},
{"op": "append", "target": "", "content": "c"},
]
_, reports = apply_patch_with_report("body", edits)
assert [r["index"] for r in reports] == [1, 2, 3]
def test_report_truncation(self) -> None:
long_target = "x" * 300
long_content = "y" * 300
edits = [{"op": "replace", "target": long_target, "content": long_content}]
_, reports = apply_patch_with_report(long_target, edits)
assert len(reports[0]["target"]) == 200
assert len(reports[0]["content_preview"]) == 200
def test_ranges_recalculated_each_edit(self) -> None:
"""冻结区坐标在每条 edit 后重新计算。"""
protected = "FREEZE"
content = f"AAA\n{protected}\nBBB"
edits = [
{"op": "append", "target": "", "content": "prefix text"},
{"op": "replace", "target": protected, "content": "nope"},
]
out, reports = apply_patch_with_report(
content, edits, protected_spans=[protected]
)
# append 在 FREEZE 之前插入,坐标右移后 replace 仍能检测冻结区
assert reports[1]["status"] == "skipped_protected"
def test_append_before_earliest_protected(self) -> None:
"""append 插到 start>0 的最早冻结区之前。"""
content = f"---\nfrontmatter\n---\n\nbody\n\n{APPENDIX_START}\nold\n{APPENDIX_END}"
edits = [{"op": "append", "target": "", "content": "INSERTED"}]
out, reports = apply_patch_with_report(
content, edits, protected_spans=[f"{APPENDIX_START}\nold\n{APPENDIX_END}"]
)
app_pos = out.find(APPENDIX_START)
ins_pos = out.find("INSERTED")
assert ins_pos < app_pos, "append 应在冻结区之前"
def test_payload_stripped_target_not_stripped(self) -> None:
"""target 不 strippayload 做 strip。"""
content = " spaced target \nrest"
edits = [
{
"op": "replace",
"target": " spaced target ",
"content": " trimmed ",
}
]
out, reports = apply_patch_with_report(content, edits)
# payload stripped → "trimmed"
assert "trimmed" in out
assert " trimmed " not in out
assert reports[0]["status"] == "applied_replace"
def test_replace_count_one(self) -> None:
"""replace 只替换第一次出现。"""
content = "dup\ndup\ndup"
edits = [{"op": "replace", "target": "dup", "content": "REPLACED"}]
out, _ = apply_patch_with_report(content, edits)
assert out.count("REPLACED") == 1
assert out.count("dup") == 2
def test_multiple_edits_sequential(self) -> None:
"""多条 edit 顺序执行。"""
content = "line1\nline2\nline3"
edits = [
{"op": "replace", "target": "line1", "content": "LINE_ONE"},
{"op": "delete", "target": "line2", "content": ""},
{"op": "append", "target": "", "content": "TAIL"},
]
out, reports = apply_patch_with_report(content, edits)
assert "LINE_ONE" in out
assert "line2" not in out
assert "TAIL" in out
assert all(r["status"].startswith("applied") for r in reports)