muse-agent-example/tests/集成/test_持久返修会话.py
zizi 9e6f1c4481 R2 改造交付:新版模块化单体全量成果
- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具)
- 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺
- 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口
- 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影)
- 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威)
- R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2026-09-15 12:47:42 +08:00

385 lines
14 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"])
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.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["原稿"]
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
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
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
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
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"]
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,
)
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") == []
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