muse-agent-example/tests/单元/test_离线修订回放.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

387 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""离线原观察承接:真实重算诊断和保真,文件产物不能成为正式批准。"""
import json
from copy import deepcopy
from dataclasses import asdict, replace
import pytest
from pydantic import TypeAdapter, ValidationError
from muse.共享.错误 import Muse错误
from muse.审校修订.接口 import 允许选区, 离线修订回放, 离线修订请求, 诊断文本, 读取内置规则
from muse.正式变更.接口 import 固定哈希
from muse.正文写作.接口 import 可见文本, 换行节点, 文本节点, 正文草稿, 段落, 段落哈希, 选段修改
def 请求副本():
doc = 正文草稿((段落("p1", (文本节点("值得注意的是,门外已经下起了雨。"),)),))
artifact = 诊断文本(
"值得注意的是,门外已经下起了雨。",
work_id="synthetic:demo",
规则库=读取内置规则(),
预览候选=True,
)
finding = next(f for f in artifact["findings"] if f["rule_id"] == "l002")
patch = 选段修改("p1", 段落哈希(doc.paragraphs[0]), 0, 7, "值得注意的是,", "", finding["id"])
return TypeAdapter(离线修订请求).validate_python(
{
"work_id": "synthetic:demo",
"document": doc,
"diagnosis": artifact,
"patches": (patch,),
"allowed_ranges": (允许选区("p1", 0, 7),),
"approved_findings": (finding["id"],),
"purpose": "删除作者确认无功能的元话语",
"author_approved": True,
"author_choice": "candidate",
"fact_snapshot": {
"payload": {"entities": []},
"content_hash": 固定哈希({"entities": []}),
},
}
)
@pytest.mark.case_id(
"NC-w23-239009",
environment="离线",
given="具名作者、固定来源、隔离测试配置与真实模块存储",
when="通过实际公开入口执行并核对拒绝、状态及持久结果",
then=["修订机械门回放保真"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_修订机械门回放保真__239009():
"""只验证机械回放;独立成对选择由原观察集成测试另行核验。"""
req = 请求副本()
before = deepcopy(asdict(req))
report = 离线修订回放(req)
assert report["hard_gate"]["mechanical_pass"]
assert report["regression_gate"]["pass"]
assert report["candidate_text"] == "门外已经下起了雨。"
assert report["author_choice"] == "candidate" and report["final"]["action"] == "candidate"
assert report["summary"]["requested_changes"] == 1
assert not report["persisted"] and not report["model_verified"]
assert report["production_validity"] == "not_checked"
assert asdict(req) == before
@pytest.mark.case_id(
"TC-7c28d3e5268e",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_artifact_header_missing_blocks_patch__7c28d3():
req = 请求副本()
for field in ("mode", "text_hash", "rule_library_version", "schema_version"):
artifact = deepcopy(req.diagnosis)
artifact.pop(field)
with pytest.raises(Muse错误):
离线修订回放(replace(req, diagnosis=artifact))
@pytest.mark.case_id(
"TC-2a3922291273",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_no_artifact_refuses_to_start__2a3922():
with pytest.raises(Muse错误, match="诊断"):
replace(请求副本(), diagnosis={})
@pytest.mark.case_id(
"TC-c6ee33ed9ed2",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_no_patches_refuses_to_start__c6ee33():
with pytest.raises(Muse错误, match="修改"):
replace(请求副本(), patches=())
@pytest.mark.case_id(
"TC-31336539614c",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["缺快照以明确audit_only结果返回并保留原文,不再依赖旧异常类名;没有产生提议稿。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_missing_snapshot_downgrades_to_audit__313365():
req = replace(请求副本(), fact_snapshot=None)
result = 离线修订回放(req)
assert result["final"]["action"] == "audit_only" and result["proposed_document"] is None
assert result["candidate_text"] == result["original_text"]
@pytest.mark.case_id(
"TC-5ede83a386d7",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["显式缺快照授权只放开离线预览,事实来源仍missing且不能成为正式批准。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_snapshot_missing_with_explicit_authorization_proceeds__5ede83():
req = replace(请求副本(), fact_snapshot=None, no_snapshot_authorization=True)
result = 离线修订回放(req)
assert result["final"]["action"] == "candidate"
assert result["candidate_text"] == "门外已经下起了雨。"
assert result["fact_snapshot_verification"] == "missing" and not result["persisted"]
@pytest.mark.case_id(
"TC-2fe0502fb00f",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["不信任传入确定性发现,重算诊断与规则副本后逐字段核对,伪rule_id被拒绝。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_forged_deterministic_finding_is_rejected__2fe050():
req = 请求副本()
artifact = deepcopy(req.diagnosis)
artifact["findings"][0]["rule_id"] = "l999"
with pytest.raises(Muse错误, match="确定性发现"):
离线修订回放(replace(req, diagnosis=artifact))
@pytest.mark.case_id(
"TC-86066255ada7",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["批准发现独立于不可变诊断保存,不篡改decision_proposal为repair;未点名批准不得修改。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_unarbitrated_finding_is_rejected__860662():
req = replace(请求副本(), approved_findings=())
with pytest.raises(Muse错误, match="作者批准"):
离线修订回放(req)
@pytest.mark.case_id(
"TC-1d7b172d5a50",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["不接受调用方自报硬门或human_score;输入合同拒绝预制检查,报告只由确定性检查生成。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_report_rejects_score_fields__1d7b17():
value = asdict(请求副本())
value["quality_score"] = 100
with pytest.raises(ValidationError):
TypeAdapter(离线修订请求).validate_python(value)
value.pop("quality_score")
value["hard_gate"] = {"pass": True, "checks": {}, "unverified": [], "human_score": 0.9}
with pytest.raises(ValidationError):
TypeAdapter(离线修订请求).validate_python(value)
artifact = deepcopy(value["diagnosis"])
artifact["human_probability"] = 1
with pytest.raises(Muse错误):
离线修订回放(replace(请求副本(), diagnosis=artifact))
@pytest.mark.case_id(
"TC-9cc771e0dac1",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=[
(
"从同一CLI main读取实际请求并输出JSON文件,禁止装配及读取DB配置,候选文本一致;输出明"
"确为离线副本而非落库。"
)
],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_cli_offline_patch_chain_writes_report_without_db__9cc771(tmp_path, monkeypatch, capsys):
import muse.接入.cli.入口 as cli
def 禁止构建(*args, **kwargs):
pytest.fail("显式离线回放不应装配或读取数据库配置")
monkeypatch.setattr(cli, "构建", 禁止构建)
monkeypatch.setattr(cli, "读取配置", 禁止构建)
request = tmp_path / "request.json"
request.write_text(json.dumps(asdict(请求副本()), ensure_ascii=False))
assert cli.main(["审校", "不存在的配置.toml", "修订回放", str(request), "--离线"]) == 0
text = capsys.readouterr().out
output = tmp_path / "report.json"
output.write_text(text)
report = json.loads(output.read_text())
assert report["candidate_text"] == "门外已经下起了雨。"
assert report["mode"] == "offline_preview" and not report["persisted"]
@pytest.mark.case_id(
"TC-8b943d1cfafd",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["缺文件统一为新版CLI结构化业务失败及非零退出,不保留旧脚本专属退出码2。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_cli_requires_artifact_file__8b943d(tmp_path, capsys):
from muse.接入.cli.入口 import main
assert main(["审校", "-", "修订回放", str(tmp_path / "missing.json"), "--离线"]) != 0
error = json.loads(capsys.readouterr().err)
assert error["code"] and "请求文件" in error["message"]
def 以原文请求(doc, start, end, rule="l002"):
text = 可见文本(doc)
artifact = 诊断文本(text, work_id="synthetic:demo", 规则库=读取内置规则(), 预览候选=True)
finding = next(f for f in artifact["findings"] if f["rule_id"] == rule)
patch = 选段修改(
"p1", 段落哈希(doc.paragraphs[0]), start, end, text[start:end], "", finding["id"]
)
return replace(
请求副本(),
document=doc,
diagnosis=artifact,
patches=(patch,),
allowed_ranges=(允许选区("p1", start, end),),
approved_findings=(finding["id"],),
)
@pytest.mark.case_id(
"TC-3d92e0528232",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_patch_without_finding_rejected__3d92e0():
req = 请求副本()
with pytest.raises(Muse错误, match="真实发现"):
离线修订回放(replace(req, patches=(replace(req.patches[0], finding_id="absent"),)))
@pytest.mark.case_id(
"TC-1ef3f9a6f139",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["重复文本不接受仅字面原串的旧patch形状;新协议要求段落、码点与哈希,不任取首次命中。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_patch_nonunique_match_rejected__1ef3f9():
text = "值得注意的是,值得注意的是,他来了。"
doc = 正文草稿((段落("p1", (文本节点(text),)),))
value = asdict(以原文请求(doc, 0, 7))
value["patches"] = [
{
"finding_id": value["approved_findings"][0],
"original": "值得注意的是,",
"replacement": "",
}
]
with pytest.raises(ValidationError):
TypeAdapter(离线修订请求).validate_python(value)
assert 可见文本(doc) == text
@pytest.mark.case_id(
"TC-c0739c86f8d4",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_patch_not_covering_span_rejected__c0739c():
req = 请求副本()
patch = replace(req.patches[0], start=1, original="得注意的是,")
with pytest.raises(Muse错误, match="未覆盖"):
离线修订回放(replace(req, patches=(patch,)))
@pytest.mark.case_id(
"TC-96af2d395ccb",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=[
"作者明确替换协议不再接受skip伪装;额外action字段连同偷渡替换被类型合同拒绝。实际模型retain偷渡另由232002验证。"
],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_skip_patch_cannot_smuggle_replacement__96af2d():
value = asdict(请求副本())
patch = dict(value["patches"][0], action="skip", replacement="他突然笑了")
value["patches"] = [patch]
with pytest.raises(ValidationError):
TypeAdapter(离线修订请求).validate_python(value)
@pytest.mark.case_id(
"TC-2da14f616f72",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_patch_does_not_normalize_unrelated_blank_lines__2da14f():
doc = 正文草稿(
(
段落(
"p1",
(
文本节点("前文。"),
换行节点(),
换行节点(),
换行节点(),
文本节点("值得注意的是,他来了。"),
),
),
)
)
result = 离线修订回放(以原文请求(doc, 6, 13))
assert result["candidate_text"] == "前文。\n\n\n他来了。"
@pytest.mark.case_id(
"TC-4de18907353d",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_adjacent_punctuation_extension_allowed__4de189():
text = "他打量她,嘴角微微上扬。「姑娘」"
doc = 正文草稿((段落("p1", (文本节点(text),)),))
result = 离线修订回放(以原文请求(doc, 4, 11, rule="l003"))
assert result["candidate_text"] == "他打量她。「姑娘」"
@pytest.mark.case_id(
"TC-35024a0ab5c7",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_patch_outside_author_scope_is_rejected__35024a():
req = replace(请求副本(), allowed_ranges=(允许选区("another-paragraph", 0, 7),))
with pytest.raises(Muse错误, match="允许范围"):
离线修订回放(req)
@pytest.mark.case_id(
"TC-b9e7a876c2f7",
environment="默认离线",
when="执行具名公开保真、离线回放或实际CLI与隔离PG入口",
then=[
"旧测试把诊断patch扩大到整句。新版在批准发现范围内故意新增三百五十两验证同类数字硬门,同时由范围用例拒绝越界扩句。"
],
contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md",
)
def test_fact_delta_blocks_new_number__b9e7a8():
doc = 正文草稿((段落("p1", (文本节点("值得注意的是,他付了三百两。"),)),))
req = 以原文请求(doc, 0, 7)
req = replace(req, patches=(replace(req.patches[0], replacement="他付了三百五十两,"),))
result = 离线修订回放(req)
assert not result["hard_gate"]["mechanical_pass"]
assert any("数字" in e for e in result["hard_gate"]["errors"])
assert result["candidate_text"] == result["original_text"]