- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具) - 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺 - 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口 - 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影) - 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威) - R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import { readFileSync, statSync } from "node:fs";
|
||
import { expect, test } from "@playwright/test";
|
||
|
||
test("NC-browser-backup-roundtrip:真实备份核对与下载", async ({
|
||
page,
|
||
}, info) => {
|
||
const passwordFile = process.env.MUSE_AUTHOR_PASSWORD_FILE;
|
||
const backupId = process.env.MUSE_BACKUP_ID;
|
||
const downloadPath = process.env.MUSE_BACKUP_DOWNLOAD_PATH;
|
||
if (!passwordFile || !backupId || !downloadPath)
|
||
throw new Error(
|
||
"需要 MUSE_AUTHOR_PASSWORD_FILE、MUSE_BACKUP_ID 和 MUSE_BACKUP_DOWNLOAD_PATH",
|
||
);
|
||
const passwordMode = statSync(passwordFile).mode & 0o777;
|
||
if (passwordMode !== 0o600)
|
||
throw new Error("MUSE_AUTHOR_PASSWORD_FILE 权限必须为 600");
|
||
|
||
const errors: string[] = [];
|
||
page.on("pageerror", (error) => errors.push(error.message));
|
||
await page.goto("/system/backups");
|
||
await page
|
||
.getByLabel("访问口令")
|
||
.fill(readFileSync(passwordFile, "utf8").trim());
|
||
await page.getByRole("button", { name: "进入工作台" }).click();
|
||
await page.goto("/system/backups");
|
||
|
||
const panel = page.getByRole("region", { name: "备份核验状态" });
|
||
await expect(
|
||
page.getByRole("heading", { name: "备份与恢复", exact: true }),
|
||
).toBeVisible();
|
||
await expect(panel.getByRole("listitem")).toHaveCount(2);
|
||
const valid = panel.getByRole("listitem").filter({ hasText: backupId });
|
||
const unreadable = panel
|
||
.getByRole("listitem")
|
||
.filter({ hasText: "清单无法读取" });
|
||
await expect(valid).toHaveCount(1);
|
||
await expect(valid).toContainText("数据库归档:");
|
||
await expect(valid).toContainText("原文/草稿文件:");
|
||
await expect(unreadable).toHaveCount(1);
|
||
await expect(unreadable.getByRole("button")).toHaveCount(0);
|
||
|
||
const verifiedResponse = page.waitForResponse(
|
||
(response) =>
|
||
response.url().includes("/verify?") &&
|
||
response
|
||
.url()
|
||
.includes(`expected_backup_id=${encodeURIComponent(backupId)}`),
|
||
);
|
||
await valid.getByRole("button", { name: "核对字节" }).click();
|
||
const verified = await verifiedResponse;
|
||
expect(verified.status()).toBe(200);
|
||
expect(await verified.json()).toMatchObject({
|
||
backup_id: backupId,
|
||
archive_verified: true,
|
||
restore_verified: false,
|
||
});
|
||
await expect(
|
||
page.getByText(
|
||
`备份 ${backupId} 的归档字节已核对;尚未完成恢复核验。`,
|
||
{ exact: true },
|
||
),
|
||
).toBeVisible();
|
||
|
||
const downloadEvent = page.waitForEvent("download");
|
||
await valid.getByRole("button", { name: "核对并下载" }).click();
|
||
const download = await downloadEvent;
|
||
expect(download.suggestedFilename()).toBe(`${backupId}.tar`);
|
||
await download.saveAs(downloadPath);
|
||
expect(statSync(downloadPath).size).toBeGreaterThan(0);
|
||
await expect(
|
||
page.getByText(/完整备份已交给浏览器下载;下载不代表已完成恢复/),
|
||
).toBeVisible();
|
||
|
||
await page.reload();
|
||
await expect(panel.getByRole("listitem")).toHaveCount(2);
|
||
await expect(page.getByText("恢复成功", { exact: true })).toHaveCount(0);
|
||
await expect(page.getByText("已恢复", { exact: true })).toHaveCount(0);
|
||
await expect(valid).toContainText("归档字节尚待核对");
|
||
await page.screenshot({
|
||
path: info.outputPath("备份核对与下载.png"),
|
||
fullPage: true,
|
||
});
|
||
expect(errors).toEqual([]);
|
||
});
|