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

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

465 lines
18 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.

"""持久有限返修:真实 S02 轮任务、作者选择、恢复与失败停止。"""
from concurrent.futures import ThreadPoolExecutor
from dataclasses import replace
from datetime import UTC, datetime, timedelta
from decimal import Decimal
import pytest
import test_模型修订任务 as 修订测试
from muse.任务运行.接口 import 任务状态, 任务预算计划, 角色预算, 预算管理, 额度策略
from muse.共享.错误 import Muse错误
from muse.审校修订.接口 import 审校服务, 返修决定
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
from muse.编排.审校修订 import 决定有限返修, 发起有限返修, 读取有限返修
pytestmark = pytest.mark.数据库
生成环境 = 修订测试.生成环境
修订环境 = 修订测试.修订环境
def 批准轮预算(env, task_id: str) -> None:
budget = 预算管理(env["库"], "synthetic")
budget.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
budget.登记任务预算(
task_id,
任务预算计划(
Decimal("4"),
(角色预算("writer", 2, 2, Decimal("1")),),
"revision-session-approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
def 候选输出(env, replacement: str = "") -> dict:
return {
"edits": [
{
"finding_id": env["发现"],
"action": "replace",
"replacement": replacement,
"reason": "合成修改建议",
}
]
}
def 发起并产出候选(env, *, command: str, max_rounds: int = 3) -> dict:
face = 发起有限返修(
env["装配"],
env["作者"],
command,
env["授权"],
配置ID="gen-config",
最大轮数=max_rounds,
)
task_id = face["rounds"][-1]["task_id"]
批准轮预算(env, task_id)
env["剧本"].append({"类型": "文本", "文本": 候选输出(env)})
修订测试.跑(env, task_id)
return 读取有限返修(env["装配"], env["作者"], face["session_id"])
@pytest.mark.case_id(
"NC-w23-236001",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["首轮关联真实任务候选且没有作者重试不创建下一轮"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_首轮关联真实任务候选且没有作者重试不创建下一轮__236001(修订环境):
env = 修订环境
started = 发起有限返修(
env["装配"],
env["作者"],
"revision-session",
env["授权"],
配置ID="gen-config",
最大轮数=3,
)
assert started["state"] == "running"
assert started["current_round"] == 1
assert len(started["rounds"]) == 1
task_id = started["rounds"][0]["task_id"]
assert task_id
批准轮预算(env, task_id)
env["剧本"].append({"类型": "文本", "文本": 候选输出(env)})
assert 修订测试.跑(env, task_id).状态.value == "completed"
face = 读取有限返修(env["装配"], env["作者"], started["session_id"])
assert face["state"] == "awaiting_author"
assert face["stop_reason"] is None
assert len(face["rounds"]) == 1
assert face["rounds"][0]["task_state"] == "completed"
assert face["rounds"][0]["result"]["outcome"] == "candidate"
assert face["rounds"][0]["candidate_id"] == face["rounds"][0]["result"]["candidate_id"]
candidate = env["正文"].读取候选(env["作者"], face["rounds"][0]["candidate_id"])
assert candidate["origin"] == "model" and candidate["decision"] == "undecided"
assert len(env["收到"]) == 1
assert len(env["装配"].任务运行.列出任务(env["作者"].作者, 作品ID="gen-work")) == 1
@pytest.mark.case_id(
"NC-w23-236002",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["作者选择关闭后重装可恢复且候选不自动采纳"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
@pytest.mark.parametrize(
"choice,reason,selected",
[
("original", "author_selected_original", False),
("candidate", "author_selected_candidate", True),
],
)
def test_作者选择关闭后重装可恢复且候选不自动采纳__236002(修订环境, choice, reason, selected):
env = 修订环境
before = 发起并产出候选(env, command="close-session")
candidate_id = before["rounds"][0]["candidate_id"]
closed = 决定有限返修(
env["装配"],
env["作者"],
"close-choice",
before["session_id"],
返修决定(1, candidate_id, choice),
)
assert closed["state"] == "closed" and closed["stop_reason"] == reason
assert closed["author_choice"] == choice
assert closed["selected_candidate_id"] == (candidate_id if selected else None)
reloaded = 审校服务(env["库"]).读取返修会话(
env["作者"], env["装配"].任务运行, before["session_id"]
)
assert reloaded == closed
assert env["正文"].读取候选(env["作者"], candidate_id)["decision"] == "undecided"
assert env["正文"].读取正文(env["作者"], "ch-4") == env["原稿"]
@pytest.mark.case_id(
"NC-w23-236003",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["作者明确重试才创建第二个真实任务且零修改停止"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_作者明确重试才创建第二个真实任务且零修改停止__236003(修订环境):
env = 修订环境
first = 发起并产出候选(env, command="retry-session")
candidate_id = first["rounds"][0]["candidate_id"]
second = 决定有限返修(
env["装配"],
env["作者"],
"retry-choice",
first["session_id"],
返修决定(1, candidate_id, "retry"),
)
assert second["state"] == "running" and second["current_round"] == 2
assert len(second["rounds"]) == 2
assert second["rounds"][0]["task_id"] != second["rounds"][1]["task_id"]
task_id = second["rounds"][1]["task_id"]
批准轮预算(env, task_id)
env["剧本"].append(
{
"类型": "文本",
"文本": {
"edits": [
{
"finding_id": env["发现"],
"action": "retain",
"replacement": "",
"reason": "本轮保留原文",
}
]
},
}
)
修订测试.跑(env, task_id)
stopped = 读取有限返修(env["装配"], env["作者"], first["session_id"])
assert stopped["state"] == "closed" and stopped["stop_reason"] == "zero_change"
assert stopped["rounds"][1]["outcome"] == "original"
assert len(env["收到"]) == 2
@pytest.mark.case_id(
"NC-w23-236004",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["轮任务登记中断以确定性命令恢复且不重复创建"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_轮任务登记中断以确定性命令恢复且不重复创建__236004(修订环境, monkeypatch):
env = 修订环境
svc = env["装配"].要求审校()
register = svc.登记返修轮任务
calls = 0
def interrupt_once(identity, session_id, task):
nonlocal calls
calls += 1
if calls == 1:
raise RuntimeError("模拟任务创建后轮登记中断")
return register(identity, session_id, task)
monkeypatch.setattr(svc, "登记返修轮任务", interrupt_once)
with pytest.raises(RuntimeError, match="轮登记中断"):
发起有限返修(
env["装配"],
env["作者"],
"interrupted-session",
env["授权"],
配置ID="gen-config",
最大轮数=3,
)
before = env["装配"].任务运行.列出任务(env["作者"].作者, 作品ID="gen-work")
assert len(before) == 1
recovered = 发起有限返修(
env["装配"],
env["作者"],
"interrupted-session",
env["授权"],
配置ID="gen-config",
最大轮数=3,
)
assert recovered["state"] == "running"
assert recovered["rounds"][0]["task_id"] == before[0].任务ID
assert len(env["装配"].任务运行.列出任务(env["作者"].作者, 作品ID="gen-work")) == 1
@pytest.mark.case_id(
"NC-w23-236005",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["同命令重放幂等异参数冲突且并发重试只增一轮"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_同命令重放幂等异参数冲突且并发重试只增一轮__236005(修订环境):
env = 修订环境
first = 发起并产出候选(env, command="concurrent-session", max_rounds=4)
candidate_id = first["rounds"][0]["candidate_id"]
decision = 返修决定(1, candidate_id, "retry")
def retry(command: str):
try:
return 决定有限返修(env["装配"], env["作者"], command, first["session_id"], decision)
except Muse错误 as exc:
return exc
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(retry, ("retry-a", "retry-b")))
successes = [r for r in results if isinstance(r, dict)]
failures = [r for r in results if isinstance(r, Muse错误)]
assert len(successes) == len(failures) == 1
assert len(successes[0]["rounds"]) == 2
winning_command = "retry-a" if isinstance(results[0], dict) else "retry-b"
replay = 决定有限返修(env["装配"], env["作者"], winning_command, first["session_id"], decision)
assert replay["rounds"][1]["task_id"] == successes[0]["rounds"][1]["task_id"]
with pytest.raises(Muse错误, match="不能改变"):
决定有限返修(
env["装配"],
env["作者"],
winning_command,
first["session_id"],
返修决定(1, candidate_id, "original"),
)
assert len(env["装配"].任务运行.列出任务(env["作者"].作者, 作品ID="gen-work")) == 2
@pytest.mark.case_id(
"NC-w23-236006",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["达到上限后停止且不能以新命令重试"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_达到上限后停止且不能以新命令重试__236006(修订环境):
env = 修订环境
face = 发起并产出候选(env, command="one-round", max_rounds=1)
assert face["state"] == "closed" and face["stop_reason"] == "round_limit"
with pytest.raises(Muse错误, match="不等待作者选择"):
决定有限返修(
env["装配"],
env["作者"],
"retry-over-limit",
face["session_id"],
返修决定(1, face["rounds"][0]["candidate_id"], "retry"),
)
selected = 决定有限返修(
env["装配"],
env["作者"],
"select-at-limit",
face["session_id"],
返修决定(1, face["rounds"][0]["candidate_id"], "candidate"),
)
assert selected["stop_reason"] == "round_limit"
assert selected["author_choice"] == "candidate"
assert selected["selected_candidate_id"] == face["rounds"][0]["candidate_id"]
assert (
env["正文"].读取候选(env["作者"], selected["selected_candidate_id"])["decision"]
== "undecided"
)
assert len(selected["rounds"]) == 1
@pytest.mark.case_id(
"NC-w23-236009",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["硬门失败关闭并保留真实拒绝结果"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_硬门失败关闭并保留真实拒绝结果__236009(修订环境):
env = 修订环境
face = 发起有限返修(
env["装配"],
env["作者"],
"hard-gate-session",
env["授权"],
配置ID="gen-config",
最大轮数=3,
)
task_id = face["rounds"][0]["task_id"]
批准轮预算(env, task_id)
env["剧本"].append({"类型": "文本", "文本": 候选输出(env, "4枚铜钱,")})
修订测试.跑(env, task_id)
stopped = 读取有限返修(env["装配"], env["作者"], face["session_id"])
assert stopped["state"] == "closed" and stopped["stop_reason"] == "hard_gate_failed"
assert stopped["rounds"][0]["result"]["outcome"] == "rejected"
assert stopped["rounds"][0]["candidate_id"] is None
assert stopped["stop_detail"]
@pytest.mark.case_id(
"NC-w23-236007",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["原文漂移关闭会话且跨作者与改参不能绕过"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_原文漂移关闭会话且跨作者与改参不能绕过__236007(修订环境):
env = 修订环境
face = 发起并产出候选(env, command="drift-session")
env["正文"].保存人工(
env["作者"],
"new-original",
"ch-4",
1,
正文草稿((段落("p1", (文本节点("作者保存了新版本。"),)),)),
)
stopped = 决定有限返修(
env["装配"],
env["作者"],
"retry-after-drift",
face["session_id"],
返修决定(1, face["rounds"][0]["candidate_id"], "retry"),
)
assert stopped["state"] == "closed" and stopped["stop_reason"] == "basis_drift"
assert "过期" in stopped["stop_detail"]
assert len(stopped["rounds"]) == 1
with pytest.raises(Muse错误):
读取有限返修(env["装配"], replace(env["作者"], 作者="other-author"), face["session_id"])
with pytest.raises(Muse错误, match="不能改变"):
发起有限返修(
env["装配"],
env["作者"],
"drift-session",
env["授权"],
配置ID="gen-config",
最大轮数=4,
)
@pytest.mark.case_id(
"NC-w23-236008",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["模型失败保留真实任务且不伪造候选或下一轮"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_模型失败保留真实任务且不伪造候选或下一轮__236008(修订环境):
env = 修订环境
face = 发起有限返修(
env["装配"],
env["作者"],
"failed-session",
env["授权"],
配置ID="gen-config",
最大轮数=3,
)
task_id = face["rounds"][0]["task_id"]
批准轮预算(env, task_id)
env["剧本"].append({"类型": "失败"})
with pytest.raises(Muse错误):
修订测试.跑(env, task_id)
failed = 读取有限返修(env["装配"], env["作者"], face["session_id"])
assert failed["state"] == "interrupted"
assert failed["stop_reason"] in {"task_reconciling", "task_failed"}
assert len(failed["rounds"]) == 1
assert failed["rounds"][0]["result"] is None
assert env["正文"].列出候选(env["作者"], "ch-4") == []
@pytest.mark.case_id(
"NC-w23-236010",
environment="隔离PG",
given="合成资料、真实S02配置与明确作者授权",
when="通过实际返修会话和作者入口执行并重新读取",
then=["结果已保存但步骤回执中断时按原结果恢复且不重发"],
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
)
def test_结果已保存但步骤回执中断时按原结果恢复且不重发__236010(修订环境, monkeypatch):
env = 修订环境
face = 发起有限返修(
env["装配"],
env["作者"],
"result-interrupted-session",
env["授权"],
配置ID="gen-config",
最大轮数=3,
)
task_id = face["rounds"][0]["task_id"]
批准轮预算(env, task_id)
env["剧本"].append({"类型": "文本", "文本": 候选输出(env)})
run = env["装配"].任务运行
finish = run.完成步骤
def interrupt(claim, result):
if claim.处理器ID == "revision.generate":
raise RuntimeError("模拟结果提交后步骤回执丢失")
return finish(claim, result)
monkeypatch.setattr(run, "完成步骤", interrupt)
with pytest.raises(RuntimeError, match="步骤回执丢失"):
修订测试.跑(env, task_id)
delivered = 读取有限返修(env["装配"], env["作者"], face["session_id"])
assert delivered["state"] == "awaiting_author"
assert delivered["rounds"][0]["result"]["outcome"] == "candidate"
before = delivered["rounds"][0]["candidate_id"]
monkeypatch.setattr(run, "完成步骤", finish)
run.控制任务(
task_id,
env["作者"].作者,
任务状态.已失败,
"恢复",
命令ID="recover-session-result",
)
assert 修订测试.跑(env, task_id).状态.value == "completed"
recovered = 读取有限返修(env["装配"], env["作者"], face["session_id"])
assert recovered["rounds"][0]["candidate_id"] == before
assert len(env["收到"]) == 1