"""四类作者选段目的共享真实修订链,保持范围外并不伪造诊断。""" from dataclasses import asdict, replace import pytest import test_模型修订任务 as 修订测试 from muse.共享.错误 import Muse错误 from muse.启动 import 装配审校修订 from muse.正文写作.接口 import 原文选段, 换行节点, 文本节点, 正文草稿, 段落, 段落哈希, 选段写作授权 from muse.编排.审校修订 import 发起模型修订 pytestmark = pytest.mark.数据库 生成环境 = 修订测试.生成环境 @pytest.fixture def 选段环境(生成环境): from pathlib import Path env = 生成环境 装配审校修订( env["装配"], 修订测试.写手测试.合成计价(), Path(__file__).parents[2] / "配置/流程模板/审校修订.yaml", ) source = 正文草稿( ( 段落("p1", (文本节点("雨声很轻。"),)), 段落("blank", ()), 段落( "p2", ( 文本节点("雨声很轻。", ("emphasis",)), 换行节点("\r\n"), 文本节点("🙂e\u0301她只剩3枚铜钱。"), ), ), ) ) env["正文"].保存人工(env["作者"], "selection-seed", "ch-4", 0, source) saved = env["正文"].读取正文(env["作者"], "ch-4") selection = 原文选段("p2", 段落哈希(source.paragraphs[2]), 0, 5, "雨声很轻。") protection = 原文选段("p1", 段落哈希(source.paragraphs[0]), 0, 5, "雨声很轻。") auth = 选段写作授权( "gen-work", "ch-4", "main", 1, saved["document_hash"], "polish", "只处理第二处雨声描写", (selection,), (protection,), ) env.update(授权=auth, 原稿=saved, 源文档=source, 选段=selection) return env @pytest.mark.case_id( "NC-w23-235001", environment="隔离PG与合成HTTP", given="带样式、重复句及空白的已保存原文", when="按四类目的改写第二处原句", then=["只在选段重放,范围外及保护区保持,没有诊断记录"], contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md", ) @pytest.mark.parametrize( "mode,text", [ ("expand", "雨声很轻,屋檐落下一滴水。"), ("compress", "雨声轻。"), ("polish", "细雨轻响。"), ("rewrite", "雨轻轻敲着屋瓦。"), ], ) def test_四类改写只重放确切选段并保留空白样式与保护区__235001(选段环境, mode, text): from pydantic import TypeAdapter env = 选段环境 env["授权"] = replace(env["授权"], mode=mode) tid = 修订测试.发起(env) env["剧本"].append( { "类型": "文本", "文本": { "edits": [ { "selection_id": env["选段"].selection_id, "action": "replace", "replacement": text, "reason": "合成选段修改", } ] }, } ) 修订测试.跑(env, tid) result = env["装配"].要求审校().读取模型修订(env["作者"], tid) assert result["source_kind"] == "selection" and result["diagnosis_id"] is None candidate = env["正文"].读取候选(env["作者"], result["candidate_id"]) doc = TypeAdapter(正文草稿).validate_python(candidate["document"]) assert doc.paragraphs[:2] == env["源文档"].paragraphs[:2] assert doc.paragraphs[2].content[0] == 文本节点(text, ("emphasis",)) assert doc.paragraphs[2].content[1:] == env["源文档"].paragraphs[2].content[1:] assert len(env["收到"]) == 1 and "selection_id" in str(env["收到"][0]) assert env["正文"].读取正文(env["作者"], "ch-4") == env["原稿"] with env["库"].连接(只读=True) as conn: assert conn.execute("SELECT count(*) FROM muse_diagnosis").fetchone()[0] == 0 @pytest.mark.case_id( "NC-w23-235002", environment="隔离PG与合成HTTP", given="明确圈选与保护范围", when="尝试错哈希、错原文、重叠或切开CRLF", then=["发起前拒绝,不留任务或发送"], contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md", ) @pytest.mark.parametrize("bad", ["protected", "hash", "text", "overlap", "crlf"]) def test_失效或受保护选段发起前拒绝且不留任务__235002(选段环境, bad): env = 选段环境 auth, s = env["授权"], env["选段"] if bad == "protected": auth = replace(auth, protected_ranges=(s,)) elif bad == "hash": auth = replace(auth, document_hash="0" * 64) elif bad == "text": auth = replace(auth, selections=(replace(s, original="雨声巨大。"),)) elif bad == "overlap": auth = replace(auth, selections=(s, replace(s, start=1, original="声很轻。"))) else: auth = replace(auth, selections=(replace(s, start=5, end=6, original="\r"),)) with pytest.raises(Muse错误): 发起模型修订(env["装配"], env["作者"], "bad-selection", auth, 配置ID="gen-config") assert env["装配"].任务运行.读取命令任务(env["作者"].作者, "bad-selection") is None assert not env["收到"] @pytest.mark.case_id( "NC-w23-235003", environment="隔离PG与合成HTTP", given="合法作者圈选任务", when="模型保留原文或引入新数值", then=["分别原文胜出或保真拒绝,无候选"], contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md", ) @pytest.mark.parametrize( "action,replacement,outcome", [ ("retain", "", "original"), ("replace", "她有4枚铜钱。", "rejected"), ], ) def test_原文胜出与保真失败不生成候选__235003(选段环境, action, replacement, outcome): env = 选段环境 tid = 修订测试.发起(env) env["剧本"].append( { "类型": "文本", "文本": { "edits": [ { "selection_id": env["选段"].selection_id, "action": action, "replacement": replacement, "reason": "合成理由", } ] }, } ) 修订测试.跑(env, tid) result = env["装配"].要求审校().读取模型修订(env["作者"], tid) assert result["outcome"] == outcome and result["candidate_id"] is None assert env["正文"].列出候选(env["作者"], "ch-4") == [] @pytest.mark.case_id( "NC-w23-235004", environment="隔离PG与合成HTTP", given="旧整章生成接口", when="无确切圈选请求扩写或选段修改", then=["明确拒绝,转由带授权的选段链承接"], contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md", ) def test_无圈选的旧整章入口不能冒充选段改写__235004(选段环境): from muse.编排.生成正文 import 发起生成正文 env = 选段环境 for granularity in ("expand_scene", "edit_selection"): with pytest.raises(Muse错误, match="圈选"): 发起生成正文( env["装配"], env["作者"], granularity, granularity, work_id="gen-work", chapter_id="ch-4", 配置ID="gen-config", ) assert env["装配"].任务运行.读取命令任务(env["作者"].作者, granularity) is None assert not env["收到"] @pytest.mark.case_id( "NC-w23-235005", environment="隔离PG与合成HTTP", given="同一作者与真实圈选坐标", when="CLI定位并以HTTP和CLI重放生成请求", then=["定位一致、同一任务,没有隐式模型调用"], contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md", ) def test_CLI定位与HTTP生成共用同一真实选段授权__235005(选段环境, tmp_path): import json from fastapi.testclient import TestClient from muse.接入.cli.审校命令 import 运行审校命令 from muse.接入.http.应用 import 创建应用 from muse.配置 import 服务配置 env = 选段环境 secret = tmp_path / "password" secret.write_text("synthetic-selection-only") secret.chmod(0o600) config = replace( env["装配"].配置, HTTP=服务配置( str(secret), 作者ID=env["作者"].作者, 公开地址="http://testserver", 允许来源=("http://testserver",), ), ) env["装配"].配置 = config auth = asdict(env["授权"]) target = { k: auth[k] for k in ("work_id", "chapter_id", "branch_id", "revision", "document_hash") } payload = { "target": target, "selections": [ {k: v for k, v in s.items() if k != "paragraph_hash"} for s in auth["selections"] ], } path = tmp_path / "scope.json" path.write_text(json.dumps(payload, ensure_ascii=False)) location = 运行审校命令(env["装配"], "选段定位", str(path)) assert location == list(auth["selections"]) http_app = 创建应用(config) request = {"command_id": "shared-selection", "config_id": "gen-config", "request": auth} with TestClient(http_app, headers={"origin": "http://testserver"}) as client: http_app.state.装配 = env["装配"] assert ( client.post("/api/v1/session", json={"password": secret.read_text()}).status_code == 200 ) assert client.post("/api/v1/selection-scope", json=payload).json() == location response = client.post("/api/v1/revision-tasks", json=request) assert response.status_code == 201, response.text path.write_text(json.dumps(request, ensure_ascii=False)) assert 运行审校命令(env["装配"], "模型修订", str(path)) == response.json() assert ( env["装配"].任务运行.读取任务(response.json()["task_id"]).冻结输入["输入"]["source_kind"] == "selection" ) assert not env["收到"]