"""受控写手修改建议、原文胜出和保真拒绝;真实PG/S02与合成HTTP。""" import json from dataclasses import asdict, 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 Muse错误 from muse.启动 import 装配审校修订 from muse.审校修订.接口 import 允许选区, 模型修订授权 from muse.正式变更.接口 import 作者动作, 固定哈希 from muse.正文写作.接口 import 候选定位, 文本节点, 正文草稿, 段落 from muse.编排.审校修订 import 发起模型修订 pytestmark = pytest.mark.数据库 生成环境 = 写手测试.生成环境 @pytest.fixture def 修订环境(生成环境): env = 生成环境 app = env["装配"] svc = app.要求审校() 装配审校修订( app, 写手测试.合成计价(), Path(__file__).parents[2] / "配置/流程模板/审校修订.yaml" ) doc = 正文草稿((段落("p1", (文本节点("值得注意的是,林深大概还剩3枚铜钱。"),)),)) env["正文"].保存人工(env["作者"], "rev-seed", "ch-4", 0, doc) saved = env["正文"].读取正文(env["作者"], "ch-4") svc.导入规则候选(env["作者"]) target = { k: saved[k] for k in ("work_id", "chapter_id", "revision", "document_hash", "branch_id") } report = svc.生成诊断(env["作者"], target, 预览候选=True) finding = next(r for r in report["report"]["findings"] if r["rule_id"] == "l002") auth = 模型修订授权( report["diagnosis_id"], 固定哈希(report["report"]), (允许选区("p1", 0, 7),), (finding["id"],), "删除没有功能的元话语,保持事实", ) env.update(授权=auth, 原稿=saved, 发现=finding["id"]) return env def 发起(env, command="revision-task"): tid = 发起模型修订(env["装配"], env["作者"], command, env["授权"], 配置ID="gen-config")[ "task_id" ] budget = 预算管理(env["库"], "synthetic") budget.登记策略(额度策略("synthetic", "1", Decimal("20"), 40)) budget.登记任务预算( tid, 任务预算计划( Decimal("4"), (角色预算("writer", 2, 2, Decimal("1")),), "revision-approval", datetime.now(UTC) + timedelta(minutes=10), ), ) return tid def 跑(env, tid): svc = env["装配"].任务运行 while svc.读取任务(tid).状态 not in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}: claim = svc.领取步骤("revision-worker", ["revision.freeze", "revision.generate"]) assert claim is not None svc.执行一步(claim) return svc.读取任务(tid) @pytest.mark.parametrize("choice", ["replace", "retain", "bad_number"]) def test_真实模型修订候选原文胜出与保真失败__232001(修订环境, choice): env = 修订环境 tid = 发起(env) value = { "edits": [ { "finding_id": env["发现"], "action": "retain" if choice == "retain" else "replace", "replacement": "4枚铜钱," if choice == "bad_number" else "", "reason": "合成修改建议", } ] } env["剧本"].append({"类型": "文本", "文本": value}) task = 跑(env, tid) assert task.状态 == 任务状态.已完成 result = env["装配"].要求审校().读取模型修订(env["作者"], tid) assert result["call_id"] and result["output_hash"] == 固定哈希(value) assert len(env["收到"]) == 1 and not env["收到"][0].get("tools") actual = json.loads(env["收到"][0]["input"]) assert "值得注意的是," in actual["装配材料"] if choice == "replace": assert result["outcome"] == "candidate" candidate = env["正文"].读取候选(env["作者"], result["candidate_id"]) assert candidate["origin"] == "model" and candidate["decision"] == "undecided" assert candidate["visible_text"] == "林深大概还剩3枚铜钱。" else: assert result["outcome"] == ("original" if choice == "retain" else "rejected") assert result["candidate_id"] is None assert env["正文"].列出候选(env["作者"], "ch-4") == [] assert env["正文"].读取正文(env["作者"], "ch-4") == env["原稿"] @pytest.mark.parametrize("bad", ["forged", "retain_payload"]) def test_伪交付或保留项偷渡不能生成候选__232002(修订环境, monkeypatch, bad): env = 修订环境 svc = env["装配"].要求审校() tid = 发起(env) output = { "edits": [ { "finding_id": env["发现"], "action": "retain", "replacement": "偷渡" if bad == "retain_payload" else "", "reason": "合成理由", } ] } env["剧本"].append({"类型": "文本", "文本": output}) if bad == "forged": save = svc.保存模型修订 def 篡改(conn, run, claim, snapshot, request, value, call): forged = { "edits": [ { "finding_id": env["发现"], "action": "replace", "replacement": "新文本", "reason": "伪造", } ] } return save(conn, run, claim, snapshot, request, forged, call) monkeypatch.setattr(svc, "保存模型修订", 篡改) with pytest.raises(Muse错误): 跑(env, tid) assert svc.读取模型修订(env["作者"], tid) is None assert env["正文"].列出候选(env["作者"], "ch-4") == [] def test_模型修订缺配置不留任务且旧正文发送前阻断__232003(修订环境): env = 修订环境 with pytest.raises(Muse错误): 发起模型修订(env["装配"], env["作者"], "missing", env["授权"], 配置ID="absent") assert env["装配"].任务运行.读取命令任务(env["作者"].作者, "missing") is None tid = 发起(env) env["正文"].保存人工( env["作者"], "revision-new-base", "ch-4", 1, 正文草稿((段落("p1", (文本节点("作者后来写了新稿。"),)),)), ) with pytest.raises(Muse错误): 跑(env, tid) assert env["收到"] == [] assert env["装配"].要求审校().读取模型修订(env["作者"], tid) is None def test_结果提交后步骤中断复用原候选且请求重放不重发__232004(修订环境, monkeypatch): env = 修订环境 tid = 发起(env) run = env["装配"].任务运行 env["剧本"].append( { "类型": "文本", "文本": { "edits": [ { "finding_id": env["发现"], "action": "replace", "replacement": "", "reason": "删除元话语", } ] }, } ) finish = run.完成步骤 def 中断(claim, result): if claim.处理器ID == "revision.generate": raise RuntimeError("模拟候选提交后步骤回执丢失") return finish(claim, result) monkeypatch.setattr(run, "完成步骤", 中断) with pytest.raises(RuntimeError, match="回执丢失"): 跑(env, tid) svc = env["装配"].要求审校() before = svc.读取模型修订(env["作者"], tid) assert before["outcome"] == "candidate" env["正文"].保存人工( env["作者"], "after-delivery", "ch-4", 1, 正文草稿((段落("p1", (文本节点("作者后来保存的正文。"),)),)), ) assert ( 发起模型修订(env["装配"], env["作者"], "revision-task", env["授权"], 配置ID="gen-config")[ "task_id" ] == tid ) monkeypatch.setattr(run, "完成步骤", finish) run.控制任务(tid, env["作者"].作者, 任务状态.已失败, "恢复", 命令ID="recover-revision") assert 跑(env, tid).状态 == 任务状态.已完成 assert len(env["收到"]) == 1 face = svc.读取修订工作面(env["作者"], run, tid) assert face["result"] == before and face["stale"] assert len(env["正文"].列出候选(env["作者"], "ch-4")) == 1 with pytest.raises(Muse错误): svc.读取修订工作面(replace(env["作者"], 作者="other-author"), run, tid) def test_HTTP发起与CLI同命令重放使用同一授权__232005(修订环境, tmp_path): from fastapi.testclient import TestClient from muse.接入.cli.审校命令 import 运行审校命令 from muse.接入.http.应用 import 创建应用 from muse.配置 import 服务配置 env = 修订环境 secret = tmp_path / "access" secret.write_text("synthetic-only-password") secret.chmod(0o600) config = replace( env["装配"].配置, HTTP=服务配置( str(secret), 作者ID=env["作者"].作者, 公开地址="http://testserver", 允许来源=("http://testserver",), ), ) env["装配"].配置 = config request = { "command_id": "shared-request", "request": asdict(env["授权"]), "config_id": "gen-config", } path = tmp_path / "revision.json" path.write_text(json.dumps(request, ensure_ascii=False)) http_app = 创建应用(config) with TestClient(http_app, headers={"origin": "http://testserver"}) as client: http_app.state.装配 = env["装配"] assert client.post("/api/v1/revision-tasks", json=request).status_code == 401 assert ( client.post("/api/v1/session", json={"password": secret.read_text()}).status_code == 200 ) response = client.post("/api/v1/revision-tasks", json=request) assert response.status_code == 201, response.text tid = response.json()["task_id"] assert 运行审校命令(env["装配"], "模型修订", str(path)) == {"task_id": tid} face = client.get(f"/api/v1/revision-tasks/{tid}") assert face.status_code == 200 and face.json()["result"] is None assert 运行审校命令(env["装配"], "修订报告", tid) == face.json() request["request"]["purpose"] = "改成另一目的" assert client.post("/api/v1/revision-tasks", json=request).status_code >= 400 assert not env["收到"] and not env["正文"].列出候选(env["作者"], "ch-4") @pytest.mark.parametrize("stale", [False, True]) def test_模型修订经真实检查审阅后采纳且新稿不被覆盖__232006(修订环境, stale): env = 修订环境 tid = 发起(env) env["剧本"].append( { "类型": "文本", "文本": { "edits": [ { "finding_id": env["发现"], "action": "replace", "replacement": "", "reason": "删除元话语", } ] }, } ) 跑(env, tid) svc, author = env["正文"], env["作者"] result = env["装配"].要求审校().读取模型修订(author, tid) candidate = svc.读取候选(author, result["candidate_id"]) report = env["装配"].要求审校().读取报告(candidate["candidate_id"], 1) assert report["verdict"] == "passed" assert report["continuity"]["findings"][0]["判定"] == "pending" position = 候选定位( candidate["candidate_id"], 1, candidate["candidate_hash"], candidate["document_id"], 1 ) review = svc.打开候选审阅(author, position) if stale: svc.保存人工( author, "new-original", "ch-4", 1, 正文草稿((段落("p1", (文本节点("作者新版本。"),)),)) ) with pytest.raises(Muse错误): svc.决定候选( author, "adopt-revision", position, 作者动作.采纳, review["review_id"], review["review_hash"], ("document",), ) assert svc.读取正文(author, "ch-4")["visible_text"] == "作者新版本。" else: svc.决定候选( author, "adopt-revision", position, 作者动作.采纳, review["review_id"], review["review_hash"], ("document",), ) assert svc.读取正文(author, "ch-4")["visible_text"] == "林深大概还剩3枚铜钱。"