- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具) - 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺 - 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口 - 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影) - 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威) - R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
401 lines
13 KiB
TypeScript
401 lines
13 KiB
TypeScript
import { afterEach, expect, it, vi } from "vitest";
|
||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||
import { MemoryRouter } from "react-router-dom";
|
||
import {
|
||
Ui备份恢复页,
|
||
Ui数据管理页,
|
||
Ui连接与模型页,
|
||
} from "../../src/功能/系统管理";
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
function json(value: unknown, status = 200) {
|
||
return new Response(JSON.stringify(value), {
|
||
status,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
}
|
||
|
||
function renderPage(page: React.ReactNode) {
|
||
const client = new QueryClient({
|
||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||
});
|
||
return render(
|
||
<QueryClientProvider client={client}>
|
||
<MemoryRouter>{page}</MemoryRouter>
|
||
</QueryClientProvider>,
|
||
);
|
||
}
|
||
|
||
const policy = {
|
||
version: "role-policy-v1",
|
||
models: { fixed: ["fixed-model"] },
|
||
roles: {
|
||
writer: {
|
||
model_policy: "fixed",
|
||
readonly_tools: true,
|
||
},
|
||
},
|
||
};
|
||
|
||
it("NC-ui-system-not-ready:未知迁移和系统错误明确可见", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const path = new URL(request.url).pathname;
|
||
if (path === "/api/v1/system")
|
||
return json({
|
||
run_purpose: "production",
|
||
ready: false,
|
||
error: "MUSE_SCHEMA_MISMATCH",
|
||
storage: { raw_configured: true, drafts_configured: true },
|
||
release: {
|
||
version: "r2",
|
||
build_id: "build-1",
|
||
resource_release: "resource-1",
|
||
resource_count: 12,
|
||
code_hash: "code-hash",
|
||
code_matches: true,
|
||
config_matches: true,
|
||
},
|
||
database: {
|
||
compatible: false,
|
||
installed_version: 17,
|
||
database_version: 999,
|
||
missing_versions: [],
|
||
unknown_versions: [999],
|
||
changed_versions: [],
|
||
},
|
||
});
|
||
if (path.endsWith("/model-policy")) return json(policy);
|
||
if (path.endsWith("/runtime-configs")) return json([]);
|
||
throw new Error(`未预期请求:${path}`);
|
||
}),
|
||
);
|
||
renderPage(<Ui连接与模型页 />);
|
||
expect(await screen.findByText("未就绪")).toBeTruthy();
|
||
expect(screen.getByText("系统尚未就绪:MUSE_SCHEMA_MISMATCH")).toBeTruthy();
|
||
expect(screen.getByText(/未知迁移:999/)).toBeTruthy();
|
||
expect(screen.getByText("数据库兼容").nextElementSibling?.textContent).toBe(
|
||
"否",
|
||
);
|
||
});
|
||
|
||
it("NC-ui-system-redacted:配置摘要忽略凭据和提供方私有地址", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const path = new URL(request.url).pathname;
|
||
if (path === "/api/v1/system")
|
||
return json({ run_purpose: "production", ready: true });
|
||
if (path.endsWith("/model-policy")) return json(policy);
|
||
if (path.endsWith("/runtime-configs"))
|
||
return json([
|
||
{
|
||
config_id: "author-runtime",
|
||
version: "v1",
|
||
content_hash: "safe-hash",
|
||
active: false,
|
||
generation: 0,
|
||
host: "direct",
|
||
host_version: "1",
|
||
roles: {
|
||
writer: {
|
||
provider: "provider-a",
|
||
model: "model-a",
|
||
thinking: "high",
|
||
secret: "DO_NOT_RENDER_KEY",
|
||
},
|
||
},
|
||
凭据: "DO_NOT_RENDER_KEY",
|
||
endpoint: "https://private.example/secret",
|
||
},
|
||
]);
|
||
throw new Error(`未预期请求:${path}`);
|
||
}),
|
||
);
|
||
renderPage(<Ui连接与模型页 />);
|
||
expect(await screen.findByText("author-runtime · v1")).toBeTruthy();
|
||
expect(screen.queryByText(/DO_NOT_RENDER_KEY/)).toBeNull();
|
||
expect(screen.queryByText(/private\.example/)).toBeNull();
|
||
expect(screen.queryByRole("button", { name: /启用/ })).toBeNull();
|
||
});
|
||
|
||
it("NC-ui-system-draft:typed 表单只保存凭据引用且回执仍为草案", async () => {
|
||
let savedBody: unknown;
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const path = new URL(request.url).pathname;
|
||
if (path === "/api/v1/system")
|
||
return json({ run_purpose: "production", ready: true });
|
||
if (path.endsWith("/model-policy")) return json(policy);
|
||
if (path.endsWith("/runtime-configs") && request.method === "GET")
|
||
return json([]);
|
||
if (path.endsWith("/runtime-configs") && request.method === "POST") {
|
||
savedBody = await request.json();
|
||
return json({
|
||
config_id: "draft-a",
|
||
version: "v1",
|
||
content_hash: "draft-hash",
|
||
state: "draft",
|
||
});
|
||
}
|
||
throw new Error(`未预期请求:${path}`);
|
||
}),
|
||
);
|
||
renderPage(<Ui连接与模型页 />);
|
||
fireEvent.change(screen.getByLabelText("配置编号"), {
|
||
target: { value: "draft-a" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("配置版本"), {
|
||
target: { value: "v1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("执行宿主"), {
|
||
target: { value: "direct" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("宿主版本"), {
|
||
target: { value: "1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("角色"), {
|
||
target: { value: "writer" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("提供方身份"), {
|
||
target: { value: "provider-a" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("模型身份"), {
|
||
target: { value: "model-a" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("思考策略"), {
|
||
target: { value: "high" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("提供方协议"), {
|
||
target: { value: "responses" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("提供方地址"), {
|
||
target: { value: "https://provider.example/v1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("凭据引用名称"), {
|
||
target: { value: "provider-key" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("凭据引用来源"), {
|
||
target: { value: "环境变量" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("环境变量名"), {
|
||
target: { value: "PROVIDER_API_KEY" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("预算策略引用"), {
|
||
target: { value: "budget-v1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("计价版本"), {
|
||
target: { value: "pricing-v1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("角色策略版本引用"), {
|
||
target: { value: "role-policy-v1" },
|
||
});
|
||
fireEvent.change(screen.getByLabelText("资源发布版本引用"), {
|
||
target: { value: "resource-v1" },
|
||
});
|
||
fireEvent.click(screen.getByRole("button", { name: "保存新版本草案" }));
|
||
expect(
|
||
await screen.findByText("已保存 draft-a · v1 草案;当前仍未启用。"),
|
||
).toBeTruthy();
|
||
expect(savedBody).toMatchObject({
|
||
config_id: "draft-a",
|
||
version: "v1",
|
||
content: {
|
||
宿主: "direct",
|
||
角色配置: {
|
||
writer: {
|
||
provider: "provider-a",
|
||
model: "model-a",
|
||
thinking: "high",
|
||
},
|
||
},
|
||
凭据: [
|
||
{ 名称: "provider-key", 来源: "环境变量", 位置: "PROVIDER_API_KEY" },
|
||
],
|
||
},
|
||
});
|
||
expect(screen.queryByRole("button", { name: /启用/ })).toBeNull();
|
||
});
|
||
|
||
it("NC-ui-system-inventory:owner 作品和任务盘点链接真实页面", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () =>
|
||
json({
|
||
works: [{ work_id: "work-owner-a", revision: 7 }],
|
||
tasks: { running: 2, failed: 1 },
|
||
backup_status: "not_inspected",
|
||
}),
|
||
),
|
||
);
|
||
renderPage(<Ui数据管理页 />);
|
||
expect(await screen.findByText("work-owner-a")).toBeTruthy();
|
||
expect(
|
||
screen.getByRole("link", { name: "打开作品" }).getAttribute("href"),
|
||
).toBe("/works/work-owner-a/write");
|
||
expect(
|
||
screen.getByRole("link", { name: "查看任务页" }).getAttribute("href"),
|
||
).toBe("/tasks");
|
||
expect(screen.getByText("运行中").nextElementSibling?.textContent).toBe(
|
||
"2 项",
|
||
);
|
||
});
|
||
|
||
it("NC-ui-system-backup-unknown:未盘点备份不冒充备份或恢复成功", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () => json({ configured: false, items: [] })),
|
||
);
|
||
renderPage(<Ui备份恢复页 />);
|
||
expect(
|
||
await screen.findByText("尚无可核对备份;未经检查的数据不能当作已备份。"),
|
||
).toBeTruthy();
|
||
expect(screen.queryByText("备份成功")).toBeNull();
|
||
expect(screen.queryByText("恢复成功")).toBeNull();
|
||
expect(screen.queryByRole("button")).toBeNull();
|
||
});
|
||
|
||
const backup = {
|
||
entry_id: "backup-entry-a",
|
||
name: "backup-a",
|
||
state: "manifest_only",
|
||
backup_id: "backup-id-a",
|
||
file_count: 0,
|
||
database_bytes: 4096,
|
||
};
|
||
|
||
it("NC-ui-backup-database-bytes:零原文文件仍单独显示数据库字节", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () => json({ configured: true, items: [backup] })),
|
||
);
|
||
renderPage(<Ui备份恢复页 />);
|
||
expect(await screen.findByText(/数据库归档:4,096 字节/)).toBeTruthy();
|
||
expect(screen.getByText(/原文\/草稿文件:0 个/)).toBeTruthy();
|
||
expect(screen.getByText(/muse 数据维护 解包/).textContent).toContain(
|
||
"--预期备份ID <身份>",
|
||
);
|
||
});
|
||
|
||
it("NC-ui-backup-receipt-guard:错身份与冒充恢复的回执均拒绝", async () => {
|
||
let verifyCount = 0;
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const url = new URL(request.url);
|
||
if (url.pathname === "/api/v1/system/backups")
|
||
return json({ configured: true, items: [backup] });
|
||
if (url.pathname.endsWith("/verify")) {
|
||
verifyCount += 1;
|
||
return json(
|
||
verifyCount === 1
|
||
? {
|
||
backup_id: "another-backup",
|
||
archive_verified: true,
|
||
restore_verified: false,
|
||
}
|
||
: {
|
||
backup_id: backup.backup_id,
|
||
archive_verified: true,
|
||
restore_verified: true,
|
||
},
|
||
);
|
||
}
|
||
throw new Error(`未预期请求:${url.pathname}`);
|
||
}),
|
||
);
|
||
renderPage(<Ui备份恢复页 />);
|
||
const verify = await screen.findByRole("button", { name: "核对字节" });
|
||
fireEvent.click(verify);
|
||
expect(
|
||
await screen.findByText(/backup-a”失败:核对回执的备份身份与清单不一致/),
|
||
).toBeTruthy();
|
||
fireEvent.click(screen.getByRole("button", { name: "核对字节" }));
|
||
expect(
|
||
await screen.findByText(/backup-a”失败:核对回执未明确声明尚未恢复/),
|
||
).toBeTruthy();
|
||
expect(verifyCount).toBe(2);
|
||
});
|
||
|
||
it("NC-ui-backup-actions:核对和下载后只显示已取得的结果", async () => {
|
||
const createObjectURL = vi
|
||
.fn<(blob: Blob) => string>()
|
||
.mockReturnValue("blob:backup-file");
|
||
vi.stubGlobal(
|
||
"URL",
|
||
Object.assign(class extends URL {}, {
|
||
createObjectURL,
|
||
revokeObjectURL: vi.fn(),
|
||
}),
|
||
);
|
||
let downloadedName = "";
|
||
const click = vi
|
||
.spyOn(HTMLAnchorElement.prototype, "click")
|
||
.mockImplementation(function (this: HTMLAnchorElement) {
|
||
downloadedName = this.download;
|
||
});
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const url = new URL(request.url);
|
||
if (url.pathname === "/api/v1/system/backups")
|
||
return json({ configured: true, items: [backup] });
|
||
expect(url.searchParams.get("expected_backup_id")).toBe(backup.backup_id);
|
||
if (url.pathname.endsWith("/verify"))
|
||
return json({
|
||
backup_id: backup.backup_id,
|
||
archive_verified: true,
|
||
restore_verified: false,
|
||
});
|
||
if (url.pathname.endsWith("/download"))
|
||
return new Response(new Uint8Array([1, 2, 3, 4]), {
|
||
headers: { "Content-Type": "application/x-tar" },
|
||
});
|
||
throw new Error(`未预期请求:${url.pathname}`);
|
||
}),
|
||
);
|
||
renderPage(<Ui备份恢复页 />);
|
||
fireEvent.click(await screen.findByRole("button", { name: "核对字节" }));
|
||
expect(
|
||
await screen.findByText(/归档字节已核对;尚未完成恢复核验/),
|
||
).toBeTruthy();
|
||
expect(screen.queryByText("恢复成功")).toBeNull();
|
||
fireEvent.click(screen.getByRole("button", { name: "核对并下载" }));
|
||
expect(
|
||
await screen.findByText(/完整备份已交给浏览器下载;下载不代表已完成恢复/),
|
||
).toBeTruthy();
|
||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||
expect((createObjectURL.mock.calls[0][0] as Blob).size).toBe(4);
|
||
expect(click).toHaveBeenCalledTimes(1);
|
||
expect(downloadedName).toBe(`${backup.backup_id}.tar`);
|
||
});
|
||
|
||
it("NC-ui-backup-unreadable:坏清单明确告知且不提供操作", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () =>
|
||
json({
|
||
configured: true,
|
||
items: [
|
||
{
|
||
entry_id: "broken-entry",
|
||
name: "broken-backup",
|
||
state: "unreadable",
|
||
},
|
||
],
|
||
}),
|
||
),
|
||
);
|
||
renderPage(<Ui备份恢复页 />);
|
||
expect(await screen.findByText("broken-backup")).toBeTruthy();
|
||
expect(screen.getByText("清单无法读取,不能下载或认定已备份。")).toBeTruthy();
|
||
expect(screen.queryByRole("button")).toBeNull();
|
||
});
|