接续 88dd570,保存 W20–W24 已实现的共享接口、业务入口、迁移、工作台、测试与文档。 W20/W22/W23 保持 in_progress,W21/W24 保持 verified;此提交不宣称方法或规则正式启用、多轮返修、真实角色评测完成。 W25 新增实验、标定、逐调用交付与角色执行及其迁移/测试/索引留在实施工作树,原有私人和旧实现保留项不纳入。 验证:离线 571、前端 33 通过;PG 469 项通过、2 项浏览器未启用,2 项误带入的 W25 用例已移出本提交;最终任务与交付边界 37 项通过。make 检查、最终类型、84 项资源及 diff 检查通过。未重跑浏览器或 Pi 宿主,不以合成调用认证外部模型效果。 独立整体审查四维通过;证据保存在 R2-20260909/提交W20-W24。
330 lines
13 KiB
Python
330 lines
13 KiB
Python
"""受控写手修改建议、原文胜出和保真拒绝;真实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枚铜钱。"
|