"""固定正文逐片文学审校:真实S02/PG与合成HTTP贯通,不据此声明文学收益。""" import json from dataclasses import replace from datetime import UTC, datetime, timedelta from decimal import Decimal from pathlib import Path import pytest import test_两阶段写手 as 写手测试 from muse.任务运行.接口 import 任务状态, 任务预算计划, 角色预算, 预算管理, 额度策略 from muse.共享.调用身份 import 内容用途 from muse.共享.错误 import Muse错误 from muse.启动 import 装配完整审校 from muse.审校修订.接口 import 审校错误, 生成审校分片 from muse.正文写作.接口 import 文本节点, 正文草稿, 段落 from muse.编排.完整审校 import 发起完整审校 pytestmark = pytest.mark.数据库 生成环境 = 写手测试.生成环境 def 片结果(片, *, 有声音): return { "shard_id": 片.片ID, "dimensions": { d: {"status": s, "summary": "合成片判断", "findings": []} for d, s in [ ("continuity", "reviewed"), ("craft", "reviewed"), ("voice", "reviewed" if 有声音 else "uncalibrated"), ] }, } @pytest.fixture def 文学环境(生成环境): env = 生成环境 app = env["装配"] 装配完整审校( app, 写手测试.合成计价(), Path(__file__).parents[2] / "配置/流程模板/完整审校.yaml" ) 管理 = env["管理"] 原 = 管理.读取版本("gen-config", "1").内容 新 = replace( 原, 角色配置={ "judge": { "provider": "synthetic", # judge-fixed 白名单内的模型;合成宿主按请求回声身份。 "model": "deepseek-v4.1-flash", "thinking": "high", "stage_tools": {"执行": []}, } }, ) 管理.保存草案("review-config", "1", 新) 验证 = 管理.验证版本("review-config", "1") 管理.启用("review-config", "1", 验证回执=验证, 批准引用="synthetic-review-approval", 预期代次=0) 作者 = replace(env["作者"], 内容用途=内容用途.检测) 文本 = "林深站在门外。雨下了很久。屋里的人没有回答。门框上还留着旧划痕。" env["正文"].保存人工( 作者, "review-document", "ch-2", 0, 正文草稿((段落("r1", (文本节点(文本),)),)) ) 文 = env["正文"].读取正文(作者, "ch-2") env.update(审校作者=作者, 审校正文=文) return env def 发起(env): app = env["装配"] 文 = env["审校正文"] r = 发起完整审校( app, env["审校作者"], "review-run", work_id="gen-work", chapter_id=env.get("审校章", "ch-2"), revision=文["revision"], document_hash=文["document_hash"], 配置ID="review-config", candidate_id=文.get("candidate_id"), 核心上限=12, 上下文字数=3, ) 任务 = app.任务运行.读取任务(r["task_id"]) 输入 = 任务.冻结输入["输入"]["文学审校"] 片 = 生成审校分片(文["visible_text"], 输入["basis_hash"], 核心上限=12, 上下文字数=3) 预算 = 预算管理(env["库"], "synthetic") 预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40)) 预算.登记任务预算( r["task_id"], 任务预算计划( Decimal("10"), (角色预算("judge", 10, 10, Decimal("1")),), "approval", datetime.now(UTC) + timedelta(minutes=10), ), ) return r["task_id"], 片 def 跑(env, tid): 运行 = env["装配"].任务运行 while 运行.读取任务(tid).状态 not in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}: 领取 = 运行.领取步骤("review-worker", ["review.prepare", "review.literary"]) assert 领取 is not None 运行.执行一步(领取) return 运行.读取任务(tid) @pytest.mark.case_id( "NC-w22-227001", environment="隔离PG与合成HTTP提供方;production/muse_app测试角色", given="本书已保存正文分为多片、零问题、没有确认声音", when="经发布模板、S02当前任务、B09合法背景和B06片结果/报告用例执行", then=["逐片真实judge交付后有唯一完整B06报告;正文不变,声音未标定,真实入参不截前缀"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_全文零命中仍有真实版本化报告和逐片证据__227001(文学环境): env = 文学环境 tid, 片 = 发起(env) env["剧本"].extend({"类型": "文本", "文本": 片结果(p, 有声音=False)} for p in 片) task = 跑(env, tid) assert task.状态 == 任务状态.已完成 report = env["装配"].要求审校().读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) assert report["report"]["persisted"] and report["report"]["model_verified"] assert report["report"]["coverage"]["范围完整"] assert report["report"]["counts"] == {"repair": 0, "retain": 0, "ask": 0} assert report["report"]["dimension_status"]["voice"] == "uncalibrated" assert len(report["report"]["evidence"]) == len(片) == len(env["收到"]) assert report["current_status"] == "valid" for req, p in zip(env["收到"], 片, strict=True): data = json.loads(req["input"]) assert data["审校分片"]["片ID"] == p.片ID and data["审校分片"]["文本"] == p.文本 assert not req.get("tools") assert env["正文"].读取正文(env["审校作者"], "ch-2") == env["审校正文"] with env["库"].连接(只读=True) as 连: assert 连.execute( "SELECT COUNT(*) FROM muse_literary_shard WHERE task_id=%s", (tid,) ).fetchone()[0] == len(片) assert ( 连.execute( "SELECT COUNT(*) FROM muse_literary_report WHERE task_id=%s", (tid,) ).fetchone()[0] == 1 ) @pytest.mark.case_id( "NC-w22-227002", environment="隔离PG与合成HTTP提供方;production/muse_app测试角色", given="首片合法、后片实际引文不符", when="经发布模板、S02当前任务、B09合法背景和B06片结果/报告用例执行", then=["只有首片进入已审记录,失败状态和未审尾部独立展示,不生成完整报告"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_后片坏引文失败仍展示前片和未审尾部__227002(文学环境): env = 文学环境 tid, 片 = 发起(env) 坏 = 片结果(片[1], 有声音=False) 坏["dimensions"]["craft"]["findings"] = [ { "start": 片[1].核心起点, "end": 片[1].核心起点 + 1, "quote": "伪", "judgment": "ask", "reason": "合成坏引文", "basis_ids": [], } ] env["剧本"].extend( [{"类型": "文本", "文本": 片结果(片[0], 有声音=False)}, {"类型": "文本", "文本": 坏}] ) with pytest.raises(Muse错误): 跑(env, tid) view = env["装配"].要求审校().读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) assert view["task_state"] == "failed" and view["report_id"] is None assert view["report"]["coverage"]["已审区"] == [[片[0].核心起点, 片[0].核心终点]] assert view["report"]["coverage"]["未审区"] assert not view["report"]["persisted"] and not view["report"]["model_verified"] assert len(env["收到"]) == 2 @pytest.mark.case_id( "NC-w22-227003", environment="隔离PG与合成HTTP提供方;production/muse_app测试角色", given="真实模型响应后在保存公共接缝篡改结果字典", when="经发布模板、S02当前任务、B09合法背景和B06片结果/报告用例执行", then=["S02实际结构化哈希拒任意新结论,B06不保存假片"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_文学登记不能把任意字典冒真模型结果__227003(文学环境, monkeypatch): env = 文学环境 tid, 片 = 发起(env) env["剧本"].append({"类型": "文本", "文本": 片结果(片[0], 有声音=False)}) 服务 = env["装配"].要求审校() 原 = 服务.保存文学片 def 篡改(连, 运行, 领取, 片ID, 快照ID, 用户输入, 输出, 调用ID): 输出["dimensions"]["craft"]["summary"] = "非模型原结论" return 原(连, 运行, 领取, 片ID, 快照ID, 用户输入, 输出, 调用ID) monkeypatch.setattr(服务, "保存文学片", 篡改) with pytest.raises(Muse错误): 跑(env, tid) view = 服务.读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) assert view["report"]["coverage"]["已审字数"] == 0 @pytest.mark.case_id( "NC-w22-227004", environment="隔离PG与合成HTTP提供方;production/muse_app测试角色", given="报告生成后正文版本推进及异作者读取", when="经发布模板、S02当前任务、B09合法背景和B06片结果/报告用例执行", then=["历史报告保持,当前状态SOURCE_STALE,异作者拒读;工作面使用只读可重复读快照"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_正文漂移不改历史报告但标当前失效且隔离作者__227004(文学环境): env = 文学环境 tid, 片 = 发起(env) env["剧本"].extend({"类型": "文本", "文本": 片结果(p, 有声音=False)} for p in 片) 跑(env, tid) 服务 = env["装配"].要求审校() 初 = 服务.读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) env["正文"].保存人工( env["审校作者"], "new-review-body", "ch-2", 1, 正文草稿((段落("r1", (文本节点("作者重写了这一章。"),)),)), ) 后 = 服务.读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) assert 后["current_status"] == "SOURCE_STALE" and 后["report"] == 初["report"] with pytest.raises(Muse错误): 服务.读取文学工作面( replace(env["审校作者"], 作者="foreign-author"), env["装配"].任务运行, tid ) @pytest.mark.case_id( "NC-w22-227005", environment="隔离PG与合成HTTP提供方;production/muse_app测试角色", given="一次片后失败,再由作者明确恢复同一任务", when="经发布模板、S02当前任务、B09合法背景和B06片结果/报告用例执行", then=["原片不重发,保原尝试/调用归属,只调用剩余片并形成完整报告"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_恢复复用已存片而不重发或伪造原尝试__227005(文学环境): env = 文学环境 tid, 片 = 发起(env) env["剧本"].extend( [ {"类型": "文本", "文本": 片结果(片[0], 有声音=False)}, {"类型": "文本", "文本": {"不是": "片结果"}}, ] ) with pytest.raises(Muse错误): 跑(env, tid) 服务 = env["装配"].要求审校() 前 = 服务.读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) assert len(前["report"]["evidence"]) == 1 env["装配"].任务运行.控制任务( tid, env["审校作者"].作者, 任务状态.已失败, "恢复", 命令ID="resume-review" ) env["剧本"].extend({"类型": "文本", "文本": 片结果(p, 有声音=False)} for p in 片[1:]) assert 跑(env, tid).状态 == 任务状态.已完成 后 = 服务.读取文学工作面(env["审校作者"], env["装配"].任务运行, tid) assert 后["report"]["evidence"][0] == 前["report"]["evidence"][0] assert len({r["attempt_id"] for r in 后["report"]["evidence"]}) == 2 assert len(env["收到"]) == len(片) + 1 @pytest.mark.case_id( "NC-w22-227006", environment="隔离PG与合成HTTP提供方;production/muse_app测试角色", given="缺配置/缺judge/错正文版本/错哈希四种前置", when="经发布模板、S02当前任务、B09合法背景和B06片结果/报告用例执行", then=["明确拒绝且任务与配置原子绑定,不留下可领取孤儿任务"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize("错误", ["缺配置", "缺judge", "错版本", "错哈希"]) def test_错误前置不留下可领取孤儿审校任务__227006(文学环境, 错误): env = 文学环境 文 = env["审校正文"] config = ( "missing" if 错误 == "缺配置" else "gen-config" if 错误 == "缺judge" else "review-config" ) with pytest.raises(Muse错误): 发起完整审校( env["装配"], env["审校作者"], "bad-start", work_id="gen-work", chapter_id="ch-2", revision=2 if 错误 == "错版本" else 1, document_hash="0" * 64 if 错误 == "错哈希" else 文["document_hash"], 配置ID=config, ) assert env["装配"].任务运行.读取命令任务(env["审校作者"].作者, "bad-start") is None @pytest.mark.case_id( "NC-w22-229005", environment="隔离PG与合成提供方", given="审校真实方法消费绑定逐片调用且预算省略不消费", when="经B06公共用例及真实声明环境执行", then=["审校真实方法消费绑定逐片调用且预算省略不消费"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize("省略", [False, True]) def test_审校真实方法消费绑定逐片调用且预算省略不消费__229005(文学环境, 省略): from test_上下文快照与索引写入 import 准备已确认消费方法 env = 文学环境 原理 = "用动作的停顿显示迟疑。" * (10000 if 省略 else 1) _, version = 准备已确认消费方法( env["装配"], env["作者"], "gen-work", 标题="审校动作依据", 原理=原理 ) tid, pieces = 发起(env) env["剧本"].extend({"类型": "文本", "文本": 片结果(p, 有声音=False)} for p in pieces) assert 跑(env, tid).状态 == 任务状态.已完成 usages = env["装配"].要求知识方法().列出消费(env["作者"], version) if 省略: assert usages == [] assert all(原理 not in json.loads(r["input"])["装配材料"] for r in env["收到"]) else: assert len(usages) == len(pieces) assert {r["kind"] for r in usages} == {"review"} assert len({r["result_ref"] for r in usages}) == len(pieces) for r in env["收到"]: assert 原理 in json.loads(r["input"])["装配材料"] assert env["正文"].读取正文(env["审校作者"], "ch-2") == env["审校正文"] @pytest.mark.case_id( "NC-w22-229006", environment="隔离PG与合成提供方", given="候选全文审校不先采纳且修改后旧报告过期", when="经B06公共用例及真实声明环境执行", then=["候选全文审校不先采纳且修改后旧报告过期"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize("空章", [False, True]) def test_候选全文审校不先采纳且修改后旧报告过期__229006(文学环境, 空章): env = 文学环境 svc = env["正文"] chapter = "ch-3" if 空章 else "ch-2" base = 0 if 空章 else 1 draft = 正文草稿((段落("c1", (文本节点("林深停了下来。他望向门上的旧划痕,没有再向前走。"),)),)) receipt = svc.创建人工候选(env["作者"], "review-candidate", chapter, base, draft) candidate = svc.读取候选(env["作者"], receipt["target_ref"]) original = svc.读取正文(env["作者"], chapter) env["审校正文"] = candidate # 共用批准与片驱动,目标章改为候选实际归属。 env["审校章"] = chapter tid, pieces = 发起(env) env["剧本"].extend({"类型": "文本", "文本": 片结果(p, 有声音=False)} for p in pieces) assert 跑(env, tid).状态 == 任务状态.已完成 report = env["装配"].要求审校().读取文学工作面(env["作者"], env["装配"].任务运行, tid) assert report["report"]["model_verified"] assert report["current_status"] == "valid" assert svc.读取正文(env["作者"], chapter) == original svc.编辑候选( env["作者"], "edit-reviewed-candidate", candidate["candidate_id"], candidate["revision"], candidate["candidate_hash"], 正文草稿((段落("c1", (文本节点("林深继续走了过去。"),)),)), ) stale = env["装配"].要求审校().读取文学工作面(env["作者"], env["装配"].任务运行, tid) assert stale["current_status"] == "SOURCE_STALE" and stale["report"] == report["report"] @pytest.mark.case_id( "NC-w22-229007", environment="隔离PG与合成提供方", given="审校回执未知重试同命令不受后来正文变化影响", when="经B06公共用例及真实声明环境执行", then=["审校回执未知重试同命令不受后来正文变化影响"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_审校回执未知重试同命令不受后来正文变化影响__229007(文学环境): env = 文学环境 tid, _ = 发起(env) doc = env["审校正文"] env["正文"].保存人工( env["作者"], "review-new-body", "ch-2", 1, 正文草稿((段落("r1", (文本节点("林深走开了。"),)),)), ) result = 发起完整审校( env["装配"], env["审校作者"], "review-run", work_id="gen-work", chapter_id="ch-2", revision=doc["revision"], document_hash=doc["document_hash"], 配置ID="review-config", 核心上限=12, 上下文字数=3, ) assert result["task_id"] == tid with pytest.raises(审校错误, match="同一审校命令"): 发起完整审校( env["装配"], env["审校作者"], "review-run", work_id="gen-work", chapter_id="ch-2", revision=2, document_hash=doc["document_hash"], 配置ID="review-config", 核心上限=12, 上下文字数=3, )