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