muse-agent-example/tests/单元/test_修订保真与停止.py
zizi cf0f4fb985 W20–W24:保存知识方法、迁移框架、审校修订与案例行为的集成成果
接续 88dd570,保存 W20–W24 已实现的共享接口、业务入口、迁移、工作台、测试与文档。
W20/W22/W23 保持 in_progress,W21/W24 保持 verified;此提交不宣称方法或规则正式启用、多轮返修、真实角色评测完成。

W25 新增实验、标定、逐调用交付与角色执行及其迁移/测试/索引留在实施工作树,原有私人和旧实现保留项不纳入。

验证:离线 571、前端 33 通过;PG 469 项通过、2 项浏览器未启用,2 项误带入的 W25 用例已移出本提交;最终任务与交付边界 37 项通过。make 检查、最终类型、84 项资源及 diff 检查通过。未重跑浏览器或 Pi 宿主,不以合成调用认证外部模型效果。
独立整体审查四维通过;证据保存在 R2-20260909/提交W20-W24。
2026-09-13 16:24:42 +08:00

218 lines
8.8 KiB
Python

"""明确选区、不可抵消的保真失败与有界停止;不判断文学收益。"""
from dataclasses import replace
import pytest
from pydantic import ValidationError
from muse.审校修订.接口 import 核对修订保真, 选择返修
from muse.正文写作.接口 import (
应用选段,
换行节点,
文本节点,
正文草稿,
正文错误,
段落,
段落哈希,
选段修改,
)
def 稿(text):
return 正文草稿((段落("p1", (文本节点(text),)),))
@pytest.mark.parametrize(
"before,after",
[
("他还有3枚铜钱。", "他还有铜钱。"),
("次日他离开。", "他离开。"),
("他大概还活着。", "他还活着。"),
("他说需要等一等。", "他说“别走”。"),
],
)
def test_非删除替换不能悄悄丢数值时间情态或新增引文__230009(before, after):
original = 稿(before)
patch = 改(original, 0, len(before), after)
result = 核对修订保真(original, 应用选段(original, (patch,)), (patch,))
assert not result["mechanical_pass"] and result["errors"]
deletion = 改(original, 0, len(before), "")
assert 核对修订保真(original, 应用选段(original, (deletion,)), (deletion,))["mechanical_pass"]
def 改(doc, start, end, text):
p = doc.paragraphs[0]
return 选段修改(
p.paragraph_id, 段落哈希(p), start, end, p.content[0].text[start:end], text, "finding-1"
)
def test_structural_verify_detects_outside_change__d3bbf2():
original = 稿("她关上门。值得注意的是,天黑了。")
text = "她关上门。值得注意的是,天黑了。"
start = text.index("值得注意的是,")
patch = 改(original, start, start + 7, "")
candidate = 稿("她关上门。天黑了。另外多出一句。")
result = 核对修订保真(original, candidate, (patch,))
assert not result["mechanical_pass"] and "候选含批准选段之外的修改" in result["errors"]
def test_fact_delta_blocks_new_number__77456d():
original = 稿("值三百两银子")
patch = 改(original, 0, 6, "值三百五十两银子")
result = 核对修订保真(original, 应用选段(original, (patch,)), (patch,))
assert not result["mechanical_pass"]
assert any("新的数字" in s for s in result["errors"])
def test_number_attribution_allows_approved_deletion__9f437a():
text = "值三百两银子。值得一提的是,三天后开门。"
original = 稿(text)
start = text.index("值得一提的是,三天后")
patch = 改(original, start, start + len("值得一提的是,三天后"), "")
candidate = 应用选段(original, (patch,))
assert candidate == 稿("值三百两银子。开门。")
assert 核对修订保真(original, candidate, (patch,))["mechanical_pass"]
def test_number_attribution_blocks_unexplained_disappearance__653996():
result = 核对修订保真(稿("值三百两银子。"), 稿("值银子。"), ())
assert not result["mechanical_pass"]
def test_quote_tamper_fails__9cdea0():
result = 核对修订保真(稿("面板显示:【等级:待定】。"), 稿("面板显示:【等级:最高】。"), ())
assert not result["mechanical_pass"]
assert any("引文变化" in s for s in result["errors"])
def test_quote_duplicate_loss_cannot_hide_behind_one_deleted_quote__d5ab68():
original = 稿("前文【同一条】。后文【同一条】。")
patch = 改(original, 0, len("前文【同一条】"), "")
result = 核对修订保真(original, 稿("前文。后文。"), (patch,))
assert not result["mechanical_pass"]
assert any("引文变化" in s for s in result["errors"])
def test_quote_approved_deletion_passes__34ff49():
text = "前文。面板显示:【等级:待定】。后文。"
original = 稿(text)
start = text.index("面板显示")
patch = 改(original, start, start + len("面板显示:【等级:待定】。"), "")
candidate = 应用选段(original, (patch,))
assert candidate == 稿("前文。后文。")
assert 核对修订保真(original, candidate, (patch,))["mechanical_pass"]
def test_protected_tic_deletion_fails__fbb3c1():
original = 稿("「我说小子,你来了。」")
patch = 改(original, 1, 6, "")
result = 核对修订保真(original, 应用选段(original, (patch,)), (patch,), 保护表达=("我说小子",))
assert not result["mechanical_pass"] and "保护表达被修改或删除" in result["errors"]
def test_snapshot_missing_downgrades_to_audit__c5ce94():
result = 选择返修(
已用轮数=0, 最大轮数=3, 硬门通过=True, 有修改=True, 作者选择="retry", 有完整依据=False
)
assert result["action"] == "audit_only"
def test_重复原句依段落身份定位且范围外格式保持__230001():
一 = 段落("p1", (文本节点("他停住。", ("strong",)), 换行节点("\r\n"), 文本节点("又望了一眼。")))
二 = 段落("p2", (文本节点("他停住。"),))
doc = 正文草稿((一, 段落("blank", ()), 二))
patch = 选段修改("p2", 段落哈希(二), 1, 3, "停住", "回头", "finding-2")
new = 应用选段(doc, (patch,))
assert new.paragraphs[:2] == doc.paragraphs[:2]
assert new.paragraphs[2].content[0].text == "他回头。"
assert doc.paragraphs[2].content[0].text == "他停住。"
@pytest.mark.parametrize("bad", ["hash", "id", "original", "overlap", "bool", "range"])
def test_错段错文重叠与非法码点拒绝__230002(bad):
doc = 稿("🙂e\u0301他说:甲停住。")
p = 改(doc, 3, 4, "她")
patches = (p,)
with pytest.raises((正文错误, ValidationError)):
if bad == "hash":
patches = (replace(p, paragraph_hash="0" * 64),)
elif bad == "id":
patches = (replace(p, paragraph_id="absent"),)
elif bad == "original":
patches = (replace(p, original="她"),)
elif bad == "overlap":
patches = (p, p)
elif bad == "bool":
patches = (replace(p, start=True),)
else:
patches = (replace(p, end=999),)
应用选段(doc, patches)
def test_组合字符不归一且不能切开CRLF__230003():
doc = 稿("🙂e\u0301他说:甲停住。")
p = 改(doc, 3, 4, "她")
assert 应用选段(doc, (p,)).paragraphs[0].content[0].text == "🙂e\u0301她说:甲停住。"
para = 段落("p1", (文本节点("甲"), 换行节点("\r\n"), 文本节点("乙")))
with pytest.raises(正文错误, match="CRLF"):
应用选段(正文草稿((para,)), (选段修改("p1", 段落哈希(para), 1, 2, "\r", ""),))
@pytest.mark.parametrize(
"before,after",
[
("付三百两。", "付三百五十两。"),
("3天后走。", "4天后走。"),
("也许他会来。", "他一定会来。"),
("「甲走了。」", "「乙走了。」"),
],
)
def test_数字时间情态引文变化不能用润色掩盖__230004(before, after):
doc = 稿(before)
patch = 改(doc, 0, len(before), after)
new = 应用选段(doc, (patch,))
result = 核对修订保真(doc, new, (patch,))
assert not result["mechanical_pass"] and result["errors"]
def test_全局数字守恒也不能抵消逐选段篡改__230005():
doc = 稿("甲有3枚。乙有4枚。")
patches = (改(doc, 2, 3, "4"), 改(doc, 7, 8, "3"))
new = 应用选段(doc, patches)
assert sorted(c for c in new.paragraphs[0].content[0].text if c.isdigit()) == ["3", "4"]
assert not 核对修订保真(doc, new, patches)["mechanical_pass"]
def test_批准整引文删除可归因但不能删保护表达__230006():
doc = 稿("「甲走了。」他没有回头。")
patch = 改(doc, 0, 6, "")
new = 应用选段(doc, (patch,))
assert 核对修订保真(doc, new, (patch,))["mechanical_pass"]
assert not 核对修订保真(doc, new, (patch,), 保护表达=("甲走了",))["mechanical_pass"]
assert 核对修订保真(doc, new, (patch,))["semantic_status"] == "unverified"
def test_未经批准的范围外变化失败__230007():
doc = 稿("风停了。他抬头。")
patch = 改(doc, 5, 7, "回头")
new = 稿("雨停了。他回头。")
assert not 核对修订保真(doc, new, (patch,))["mechanical_pass"]
@pytest.mark.parametrize(
"args,action",
[
({"硬门通过": False, "作者选择": "candidate"}, "original"),
({"有完整依据": False}, "audit_only"),
({"有修改": False}, "original"),
({"作者选择": "original"}, "original"),
({"已用轮数": 3}, "stop"),
({"作者选择": None}, "awaiting_author"),
({"作者选择": "retry"}, "retry"),
],
)
def test_停止条件不由质量分数抵消__230008(args, action):
base = {"已用轮数": 1, "最大轮数": 3, "硬门通过": True, "有修改": True}
assert 选择返修(**{**base, **args})["action"] == action