"""离线原观察承接:真实重算诊断和保真,文件产物不能成为正式批准。""" 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"]