muse-agent-example/tests/集成/test_模型修订任务.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

378 lines
15 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.case_id(
"NC-w23-232001",
environment="隔离PG与合成HTTP提供方",
given="真实诊断和明确批准槽位",
when="受控writer返回修改、保留或数字破坏",
then=["分别生成模型候选、原文结果或保真拒绝;当前正文不变"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
@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.case_id(
"NC-w23-232002",
environment="隔离PG与合成HTTP提供方",
given="S02实际结构化交付",
when="保存时篡改结果或retain夹带替换",
then=["拒绝且无候选与B06终态"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
@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") == []
@pytest.mark.case_id(
"NC-w23-232003",
environment="隔离PG与合成HTTP提供方",
given="当前正文与明确配置",
when="使用未知配置或发送前更新正文",
then=["无孤儿任务或模型发送"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
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
@pytest.mark.case_id(
"NC-w23-232004",
environment="隔离PG与合成HTTP提供方",
given="候选已提交但步骤回执中断",
when="当前正文变化后重放请求并恢复",
then=["复用原候选和调用;历史结果显示过期"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
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)
@pytest.mark.case_id(
"NC-w23-232005",
environment="隔离PG与合成HTTP提供方",
given="同一作者的具名修订授权",
when="HTTP发起并由CLI重放及读取",
then=["共用同一任务;未认证及更改授权被拒"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
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.case_id(
"NC-w23-232006",
environment="隔离PG与合成HTTP提供方",
given="真实模型修订及对应必需检查",
when="作者审阅并采纳或在审阅后保存新稿",
then=["合格候选可采纳;旧候选不能覆盖作者新稿,语义仍标待证"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
@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枚铜钱。"