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

105 lines
4.7 KiB
Python

"""隔离模型任务在原库断开后,从完整备份续接已批准任务且不重发旧回合。"""
from dataclasses import replace
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from pathlib import Path
import psycopg
import pytest
import test_两阶段写手 as 写手测试
from psycopg import sql
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from muse.任务运行.接口 import 任务预算计划, 角色预算, 预算管理, 额度策略
from muse.共享.调用身份 import 用途
from muse.启动 import 构建默认运行
from muse.基础设施.备份恢复 import 创建备份, 恢复备份
from muse.编排.生成正文 import 发起生成正文
from muse.编排.运行装配 import 推进作者任务
from muse.配置 import 数据库引用
pytestmark = pytest.mark.数据库
生成环境 = 写手测试.生成环境
def test_断源恢复模型任务只补未执行步骤并保持原预算与候选__29f005(
生成环境, 应用测试库, 新空目标库, 隔离数据库URL, tmp_path
):
env = 生成环境
config = env["装配"].配置
app = 构建默认运行(config, 计价=写手测试.合成计价())
author = env["作者"]
original = app.要求正文().读取正文(author, "ch-3")
task_id = 发起生成正文(
app,
author,
"model-backup",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)["task_id"]
预算管理(env["库"], "synthetic").登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
app.任务运行.批准作者预算(
task_id,
author.作者,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"synthetic-backup-budget",
datetime.now(UTC) + timedelta(minutes=10),
),
)
budget = app.任务运行.读取作者预算(task_id, author.作者)
env["剧本"].extend(
写手测试.正常剧本(
[
"雨点敲着铁皮屋顶,林深把退回的信压在掌心。",
"他沿跳板走向仓库,门后传来一声应答。",
]
)
)
assert 推进作者任务(app, author.作者, task_id)["executed_step"]["step_id"] == "预组装"
assert 推进作者任务(app, author.作者, task_id)["executed_step"]["step_id"] == "受限探索"
assert len(env["收到"]) == 4
source_steps = app.任务运行.读取任务(task_id).步骤
backup_config = replace(config, 数据库=应用测试库[用途.维护].引用, 运行用途=用途.维护)
archive = 创建备份(backup_config, tmp_path / "backup")
source_name = conninfo_to_dict(Path(config.数据库.位置).read_text())["dbname"]
with psycopg.connect(隔离数据库URL, autocommit=True) as admin:
admin.execute(
sql.SQL("ALTER DATABASE {} ALLOW_CONNECTIONS false").format(sql.Identifier(source_name))
)
# 停用本例生成的旧暂存目录,恢复必须使用新目录;供应方凭据仍在受控存储。
raw = Path(config.原文暂存)
raw.rename(tmp_path / "offline-original-raw")
restored_raw = tmp_path / "restored-raw"
restore_config = replace(
backup_config,
数据库=新空目标库.工厂.引用,
原文暂存=str(restored_raw),
)
restored = 恢复备份(tmp_path / "backup", restore_config, archive["backup_id"])
assert restored["status"] == "verified" and not restored["services_started"]
params = conninfo_to_dict(Path(新空目标库.工厂.引用.位置).read_text())
params["user"] = "muse_app"
secret = tmp_path / "restored-app.txt"
secret.write_text(make_conninfo(**params))
secret.chmod(0o600)
fresh = 构建默认运行(
replace(config, 数据库=数据库引用("受控存储", str(secret)), 原文暂存=str(restored_raw)),
计价=写手测试.合成计价(),
)
assert fresh.任务运行.读取作者预算(task_id, author.作者) == budget
assert fresh.任务运行.读取任务(task_id).步骤 == source_steps
assert len(env["收到"]) == 4
for expected in ("冻结回放", "无工具写作", "检查与候选"):
result = 推进作者任务(fresh, author.作者, task_id)
assert result["executed_step"]["step_id"] == expected
assert result["state"] == "completed" and len(env["收到"]) == 5
final = next(s["result"] for s in result["steps"] if s["step_id"] == "检查与候选")
candidate = fresh.要求正文().读取候选(author, final["candidate_id"])
assert candidate["origin"] == "model" and candidate["decision"] == "undecided"
assert fresh.要求正文().读取正文(author, "ch-3") == original