- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具) - 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺 - 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口 - 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影) - 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威) - R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
244 lines
9.6 KiB
Python
244 lines
9.6 KiB
Python
"""离线原观察承接:真实重算诊断和保真,文件产物不能成为正式批准。"""
|
|
|
|
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": []}),
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
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))
|
|
|
|
|
|
def test_no_artifact_refuses_to_start__2a3922():
|
|
with pytest.raises(Muse错误, match="诊断"):
|
|
replace(请求副本(), diagnosis={})
|
|
|
|
|
|
def test_no_patches_refuses_to_start__c6ee33():
|
|
with pytest.raises(Muse错误, match="修改"):
|
|
replace(请求副本(), patches=())
|
|
|
|
|
|
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"]
|
|
|
|
|
|
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"]
|
|
|
|
|
|
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))
|
|
|
|
|
|
def test_unarbitrated_finding_is_rejected__860662():
|
|
req = replace(请求副本(), approved_findings=())
|
|
with pytest.raises(Muse错误, match="作者批准"):
|
|
离线修订回放(req)
|
|
|
|
|
|
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))
|
|
|
|
|
|
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"]
|
|
|
|
|
|
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"],),
|
|
)
|
|
|
|
|
|
def test_patch_without_finding_rejected__3d92e0():
|
|
req = 请求副本()
|
|
with pytest.raises(Muse错误, match="真实发现"):
|
|
离线修订回放(replace(req, patches=(replace(req.patches[0], finding_id="absent"),)))
|
|
|
|
|
|
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
|
|
|
|
|
|
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,)))
|
|
|
|
|
|
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)
|
|
|
|
|
|
def test_patch_does_not_normalize_unrelated_blank_lines__2da14f():
|
|
doc = 正文草稿(
|
|
(
|
|
段落(
|
|
"p1",
|
|
(
|
|
文本节点("前文。"),
|
|
换行节点(),
|
|
换行节点(),
|
|
换行节点(),
|
|
文本节点("值得注意的是,他来了。"),
|
|
),
|
|
),
|
|
)
|
|
)
|
|
result = 离线修订回放(以原文请求(doc, 6, 13))
|
|
assert result["candidate_text"] == "前文。\n\n\n他来了。"
|
|
|
|
|
|
def test_adjacent_punctuation_extension_allowed__4de189():
|
|
text = "他打量她,嘴角微微上扬。「姑娘」"
|
|
doc = 正文草稿((段落("p1", (文本节点(text),)),))
|
|
result = 离线修订回放(以原文请求(doc, 4, 11, rule="l003"))
|
|
assert result["candidate_text"] == "他打量她。「姑娘」"
|
|
|
|
|
|
def test_patch_outside_author_scope_is_rejected__35024a():
|
|
req = replace(请求副本(), allowed_ranges=(允许选区("another-paragraph", 0, 7),))
|
|
with pytest.raises(Muse错误, match="允许范围"):
|
|
离线修订回放(req)
|
|
|
|
|
|
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"]
|