muse-agent-example/tests/集成/test_模型修订任务.py
zizi cf0f4fb985 W20–W24:保存知识方法、迁移框架、审校修订与案例行为的集成成果
接续 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。
2026-09-13 16:24:42 +08:00

330 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""受控写手修改建议、原文胜出和保真拒绝;真实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枚铜钱。"