test(admin): 收紧 Muse 管理端验证门禁
补齐 P3 E2E 未处理接口断言和契约字段校验,修正管理端请求前缀,并记录验证产物清理规则。
This commit is contained in:
parent
a33560a03b
commit
c35f64ffcc
5
.gitignore
vendored
5
.gitignore
vendored
@ -7,7 +7,6 @@ ehthumbs.db
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# ====================
|
# ====================
|
||||||
# Editor & IDE
|
# Editor & IDE
|
||||||
@ -77,7 +76,6 @@ ENV/
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# ====================
|
# ====================
|
||||||
# Testing & Coverage
|
# Testing & Coverage
|
||||||
@ -86,6 +84,8 @@ coverage/
|
|||||||
.nyc_output/
|
.nyc_output/
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
# ====================
|
# ====================
|
||||||
# Sub-repos (独立 git 仓库,不纳入本仓)
|
# Sub-repos (独立 git 仓库,不纳入本仓)
|
||||||
@ -160,3 +160,4 @@ vite.config.ts.*
|
|||||||
.agents
|
.agents
|
||||||
.cursor
|
.cursor
|
||||||
skills-lock.json
|
skills-lock.json
|
||||||
|
precommit-scan-*.md
|
||||||
|
|||||||
@ -84,6 +84,20 @@
|
|||||||
- `echarts` 插件去除重复类型导出并显式导出 `ECOption`。
|
- `echarts` 插件去除重复类型导出并显式导出 `ECOption`。
|
||||||
- `core-user` store 在 `setUserInfo(null)` 时同步清空 `userRoles`,避免登出后旧角色残留。
|
- `core-user` store 在 `setUserInfo(null)` 时同步清空 `userRoles`,避免登出后旧角色残留。
|
||||||
|
|
||||||
|
二次完善 E2E 覆盖时发现部分 Muse 页面在真实 Vite 运行时不会自动解析本地未注册的 Ant Design Vue 组件。已在 P3 页面内显式导入实际使用的 Ant 组件,并同步补齐页面测试中的 Ant mock export:
|
||||||
|
|
||||||
|
- `muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue`
|
||||||
|
- `muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue`
|
||||||
|
- `muse-admin/apps/web-antd/src/views/muse/market/index.vue`
|
||||||
|
- `muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts`
|
||||||
|
- `muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts`
|
||||||
|
|
||||||
|
审查后继续收紧 E2E 时,发现 `museAdminApi` 原先把 `/admin-api/muse` 直接传给 Vben `requestClient`。但当前 `web-antd` 的 `VITE_GLOB_API_URL` 已经是 `/admin-api`,真实浏览器请求会变成 `/admin-api/admin-api/muse/**`。已修正为:
|
||||||
|
|
||||||
|
- `museAdminApi` 内部只拼接 `/muse/**` 资源路径。
|
||||||
|
- 由 Vben `requestClient` baseURL 统一组合成运行时 `/admin-api/muse/**`。
|
||||||
|
- `apps/web-antd/src/api/muse/__tests__/client.spec.ts` 同步断言资源路径,避免再次双拼 `/admin-api`。
|
||||||
|
|
||||||
后续需要主代理确认长期 Git 边界:要么让 `muse-admin` 成为真正独立仓库,要么调整根 `.gitignore` 和纳管策略。
|
后续需要主代理确认长期 Git 边界:要么让 `muse-admin` 成为真正独立仓库,要么调整根 `.gitignore` 和纳管策略。
|
||||||
|
|
||||||
### 2. 不伪造后端契约
|
### 2. 不伪造后端契约
|
||||||
@ -131,13 +145,55 @@ pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/v
|
|||||||
- OpenAPI TypeScript 生成通过。
|
- OpenAPI TypeScript 生成通过。
|
||||||
- P3 定向 Vitest 初次实现:10 个测试文件、19 个测试全部通过。
|
- P3 定向 Vitest 初次实现:10 个测试文件、19 个测试全部通过。
|
||||||
- P3 完善后定向 Vitest:13 个测试文件、26 个测试全部通过。
|
- P3 完善后定向 Vitest:13 个测试文件、26 个测试全部通过。
|
||||||
- P3 Playwright E2E:1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。
|
- P3 Playwright E2E 初版:1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。
|
||||||
|
- P3 Playwright E2E 二次完善:2 个 Chromium 用例通过,覆盖 MetaSchema、功能编排、AI 配置、全局知识库、市场治理、Account/New-API、任务治理、日志审计全部主要页面的真实路由访问和边界文案。
|
||||||
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
||||||
- `web-antd` typecheck 通过。
|
- `web-antd` typecheck 通过。
|
||||||
- 全量 `test:unit` 通过:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
- 全量 `test:unit` 通过:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
||||||
- `web-antd` build 通过。
|
- `web-antd` build 通过。
|
||||||
- `git diff --check` 通过。
|
- `git diff --check` 通过。
|
||||||
|
|
||||||
|
二次完善提交前复核:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --dir muse-admin --filter @vben/web-antd run typecheck
|
||||||
|
pnpm --dir muse-admin exec playwright test -c playwright.config.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||||
|
pnpm --dir muse-admin exec vitest run apps/web-antd/src/api/muse/newapi/__tests__/index.spec.ts apps/web-antd/src/api/muse/audit/__tests__/index.spec.ts apps/web-antd/src/api/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/api/muse/governance/__tests__/governance-api.test.ts apps/web-antd/src/api/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/jobs/__tests__/index.spec.ts apps/web-antd/src/api/muse/account/__tests__/index.spec.ts apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/views/muse/ai/__tests__/index.test.ts apps/web-antd/src/views/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/__tests__/client.spec.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/src/views/muse/__tests__/governance-pages.test.ts --dom
|
||||||
|
pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/views/muse apps/web-antd/src/router/routes/modules/muse.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||||
|
pnpm --dir muse-admin test:unit
|
||||||
|
pnpm --dir muse-admin --filter @vben/web-antd run build
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- `web-antd` typecheck 退出码 0。
|
||||||
|
- P3 Playwright E2E:2 个 Chromium 用例通过;Vite dev server 仍输出 `ResizeObserver loop completed with undelivered notifications` 开发噪声,但 Playwright 退出码为 0。
|
||||||
|
- P3 定向 Vitest:13 个测试文件、26 个测试通过。
|
||||||
|
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
||||||
|
- 全量 `test:unit`:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
||||||
|
- `web-antd` build 退出码 0,并已清理生成的 `dist/` 和 `dist.zip`,不纳入提交。
|
||||||
|
|
||||||
|
审查修复后再次复核:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --dir muse-admin exec playwright test -c playwright.config.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||||
|
pnpm --dir muse-admin --filter @vben/web-antd run typecheck
|
||||||
|
pnpm --dir muse-admin exec vitest run apps/web-antd/src/api/muse/newapi/__tests__/index.spec.ts apps/web-antd/src/api/muse/audit/__tests__/index.spec.ts apps/web-antd/src/api/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/api/muse/governance/__tests__/governance-api.test.ts apps/web-antd/src/api/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/jobs/__tests__/index.spec.ts apps/web-antd/src/api/muse/account/__tests__/index.spec.ts apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/views/muse/ai/__tests__/index.test.ts apps/web-antd/src/views/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/__tests__/client.spec.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/src/views/muse/__tests__/governance-pages.test.ts --dom
|
||||||
|
pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/views/muse apps/web-antd/src/router/routes/modules/muse.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||||
|
pnpm --dir muse-admin test:unit
|
||||||
|
pnpm --dir muse-admin --filter @vben/web-antd run build
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- P3 Playwright E2E:2 个 Chromium 用例通过;E2E 已改为未显式 mock 的 `/admin-api/muse/**` 请求返回 500 并在用例末尾断言无遗漏,同时断言每个主要页面的真实 mock 数据。
|
||||||
|
- 审查后删除了 E2E 中会绕开统一 mock 的重复 specific routes,所有 Muse 管理端请求都由同一个 `/admin-api/muse/**` route 处理;Account、Purchase、Jobs、Source Event mock 字段已改为契约字段,并补充关键列断言,避免字段漂移仍然绿。
|
||||||
|
- `web-antd` typecheck 退出码 0。
|
||||||
|
- P3 定向 Vitest:13 个测试文件、26 个测试通过。
|
||||||
|
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
||||||
|
- 全量 `test:unit`:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
||||||
|
- `web-antd` build 退出码 0,并已清理生成的 `test-results/`、`dist/` 和 `dist.zip`,不纳入提交。
|
||||||
|
|
||||||
## 提交与 Hook 说明
|
## 提交与 Hook 说明
|
||||||
|
|
||||||
本次 P3 相关提交:
|
本次 P3 相关提交:
|
||||||
@ -153,9 +209,20 @@ pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/v
|
|||||||
- Muse 路由注册测试。注意测试文件不能放在 `src/router/routes/modules/**`,该目录会被 Vite 动态路由 glob 当作业务路由加载。
|
- Muse 路由注册测试。注意测试文件不能放在 `src/router/routes/modules/**`,该目录会被 Vite 动态路由 glob 当作业务路由加载。
|
||||||
- Account/New-API、任务治理、审计页面的最小集成测试。
|
- Account/New-API、任务治理、审计页面的最小集成测试。
|
||||||
- Playwright E2E,使用本地 mock 覆盖 Vben 登录态、权限菜单、租户、字典、通知和 P3 API。
|
- Playwright E2E,使用本地 mock 覆盖 Vben 登录态、权限菜单、租户、字典、通知和 P3 API。
|
||||||
|
- 二次扩展 Playwright E2E,将 MetaSchema、功能编排、AI 配置、全局知识库、市场治理也纳入真实路由访问,并检查管理员边界文案、影响预览、原因和审计提示。
|
||||||
|
- 修复 E2E 暴露的 Ant Design Vue 组件运行时解析问题,Muse 页面改为显式导入实际使用的 Ant 组件。
|
||||||
|
- 审查修复后,E2E 对未处理的 Muse 管理端 API 不再返回空分页兜底,避免错误 URL 或漏 mock 仍然绿;同时修正 New-API 真实运行时双 `/admin-api` 前缀问题。
|
||||||
|
- 再次审查后,删除 Account/Jobs/Audit 的重复 route 注册,避免 specific route 抢先匹配导致 unified mock 失效;同步修正 E2E mock 字段契约漂移。
|
||||||
|
|
||||||
提交前发现暂存区已有其他会话 staged 的 `P1-execution-prompt.md` 和 `P3-execution-prompt.md`,已仅执行 `git restore --staged`,没有改动文件内容。
|
提交前发现暂存区已有其他会话 staged 的 `P1-execution-prompt.md` 和 `P3-execution-prompt.md`,已仅执行 `git restore --staged`,没有改动文件内容。
|
||||||
|
|
||||||
|
整理提交阶段补充:
|
||||||
|
|
||||||
|
- `muse-admin/apps/web-antd/dist/`、`dist.zip`、`test-results/` 属于验证产物,提交前必须清理。
|
||||||
|
- 暂存区如果混入 `docs/memorys/2026-05-25-muse-cloud后端P1收口.md`,只执行 `git restore --staged` 移出 P3 提交,不删除并行 P1 会话文件。
|
||||||
|
- 根目录 `.DS_Store` 曾被仓库跟踪;本轮改为从 Git 索引移除,并保留本地文件由 `.gitignore` 忽略,避免继续进入提交。
|
||||||
|
- 根 `.gitignore` 保留 `muse-admin/` 独立子仓库边界,同时补充 `test-results/`、`playwright-report/` 和 `precommit-scan-*.md`,避免验证报告和安全扫描报告误入主提交。
|
||||||
|
|
||||||
提交时按当前项目约定使用:
|
提交时按当前项目约定使用:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -168,4 +235,4 @@ touch /tmp/.opsera-pre-commit-scan-passed
|
|||||||
|
|
||||||
1. 主代理确认 `muse-admin/` 的长期 Git 纳管方式,避免后续继续依赖 `git add -f`。
|
1. 主代理确认 `muse-admin/` 的长期 Git 纳管方式,避免后续继续依赖 `git add -f`。
|
||||||
2. 后端补齐 Account 角色/权限写接口、任务独立重验接口后,管理端可接入真实命令入口。
|
2. 后端补齐 Account 角色/权限写接口、任务独立重验接口后,管理端可接入真实命令入口。
|
||||||
3. P3 已补齐最小 E2E;后续可继续扩展 Playwright 覆盖 MetaSchema 发布、市场审核/下架、任务重试和审计详情查看。
|
3. P3 已补齐覆盖全部主要页面的最小 E2E;后续可继续扩展 Playwright 覆盖 MetaSchema 发布、市场审核/下架、任务重试和审计详情查看等具体命令流。
|
||||||
|
|||||||
@ -9,6 +9,23 @@ const commonResult = (data: unknown) => ({
|
|||||||
|
|
||||||
const ACCESS_STORE_KEY = 'muse-vben-antd-5.7.0-dev-core-access';
|
const ACCESS_STORE_KEY = 'muse-vben-antd-5.7.0-dev-core-access';
|
||||||
|
|
||||||
|
const pageResult = (list: unknown[]) => ({
|
||||||
|
list,
|
||||||
|
total: list.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unhandledMuseAdminApiPathsByPage = new WeakMap<Page, Set<string>>();
|
||||||
|
|
||||||
|
const expectNoUnhandledMuseAdminApis = (page: Page) => {
|
||||||
|
const unhandledPaths = [
|
||||||
|
...(unhandledMuseAdminApiPathsByPage.get(page) ?? new Set<string>()),
|
||||||
|
];
|
||||||
|
expect(
|
||||||
|
unhandledPaths,
|
||||||
|
`Unhandled Muse admin API mocks: ${unhandledPaths.join(', ')}`,
|
||||||
|
).toEqual([]);
|
||||||
|
};
|
||||||
|
|
||||||
const routeCommonResult = async (
|
const routeCommonResult = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
patterns: string[],
|
patterns: string[],
|
||||||
@ -24,6 +41,450 @@ const routeCommonResult = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const routeMuseAdminApis = async (page: Page) => {
|
||||||
|
const unhandledMuseAdminApiPaths = new Set<string>();
|
||||||
|
unhandledMuseAdminApiPathsByPage.set(page, unhandledMuseAdminApiPaths);
|
||||||
|
|
||||||
|
await page.route('**/admin-api/muse/**', (route) => {
|
||||||
|
const { pathname } = new URL(route.request().url());
|
||||||
|
const path = pathname.replace('/admin-api/muse', '');
|
||||||
|
|
||||||
|
if (path === '/governance/meta-schemas') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
activeVersion: 1,
|
||||||
|
affectedObjectCount: 3,
|
||||||
|
displayName: '故事草稿结构',
|
||||||
|
draftVersion: 2,
|
||||||
|
fieldType: 'object',
|
||||||
|
migrationRisk: 'low',
|
||||||
|
schemaKey: 'story.draft',
|
||||||
|
scope: 'work',
|
||||||
|
status: 'active',
|
||||||
|
updatedAt: '2026-05-25T00:00:00Z',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/governance/function-chains') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
activeVersion: 1,
|
||||||
|
chainKey: 'story-pipeline',
|
||||||
|
description: '系统故事生成链路',
|
||||||
|
displayName: '故事链路',
|
||||||
|
openSlotCount: 2,
|
||||||
|
protectionNodeCount: 3,
|
||||||
|
status: 'active',
|
||||||
|
updatedAt: '2026-05-25T00:00:00Z',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/governance/protection-nodes') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
auditSummary: '系统保护节点',
|
||||||
|
displayName: '事实校验',
|
||||||
|
irreplaceable: true,
|
||||||
|
irreplaceableReason: '保持事实边界',
|
||||||
|
nodeKey: 'fact-guard',
|
||||||
|
nodeType: 'guard',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/ai/prompts') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
activeVersion: 1,
|
||||||
|
latestVersion: 2,
|
||||||
|
name: '候选生成 Prompt',
|
||||||
|
promptKey: 'candidate.generate',
|
||||||
|
updatedAt: '2026-05-25T00:00:00Z',
|
||||||
|
variableCount: 4,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/ai/agents') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
activeVersion: 1,
|
||||||
|
agentId: 'system-story-agent',
|
||||||
|
description: '系统写作智能体',
|
||||||
|
name: '系统故事智能体',
|
||||||
|
promptKey: 'candidate.generate',
|
||||||
|
scope: 'system',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activeVersion: 3,
|
||||||
|
agentId: 'user-private-agent',
|
||||||
|
name: '用户私有智能体摘要',
|
||||||
|
promptKey: 'user.prompt',
|
||||||
|
scope: 'user',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/ai/tool-grants') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
agentId: 'system-story-agent',
|
||||||
|
agentName: '系统故事智能体',
|
||||||
|
approvedBy: 'Security facade',
|
||||||
|
budget: { amount: 1000, period: 'daily' },
|
||||||
|
grantId: 'grant-e2e',
|
||||||
|
outboundPolicy: {
|
||||||
|
allowedDestinations: ['search.internal'],
|
||||||
|
dataPolicy: 'redacted',
|
||||||
|
networkMode: 'allowlist',
|
||||||
|
},
|
||||||
|
securityApprovalId: 'sec-approval-e2e',
|
||||||
|
status: 'approved',
|
||||||
|
toolType: 'web_search',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/ai/quality-policies') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
activeVersion: 1,
|
||||||
|
description: '候选交付质量门控',
|
||||||
|
dimensionCount: 3,
|
||||||
|
latestVersion: 2,
|
||||||
|
name: '候选交付策略',
|
||||||
|
policyKey: 'candidate.delivery',
|
||||||
|
updatedAt: '2026-05-25T00:00:00Z',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/knowledge/global-kbs') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
activePolicyVersion: 1,
|
||||||
|
bindingCount: 2,
|
||||||
|
currentVersion: 4,
|
||||||
|
documentCount: 8,
|
||||||
|
kbId: 'global-kb-e2e',
|
||||||
|
name: '系统世界观库',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes('/knowledge/global-kbs/') && path.endsWith('/documents')) {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
documentId: 'doc-e2e',
|
||||||
|
name: '系统设定资料',
|
||||||
|
processingStatus: 'searchable',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.includes('/knowledge/global-kbs/') &&
|
||||||
|
path.endsWith('/processing-tasks')
|
||||||
|
) {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(pageResult([{ status: 'completed', taskId: 'kb-task-e2e' }])),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/knowledge/source-bindings') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
bindingId: 'kb-binding-e2e',
|
||||||
|
kbId: 'global-kb-e2e',
|
||||||
|
sourceStatus: 'active',
|
||||||
|
sourceSummary: '系统资产来源',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/market/publish-requests') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
assetName: '待审资产',
|
||||||
|
publisherName: '发布者',
|
||||||
|
requestId: 'review-e2e',
|
||||||
|
riskTags: ['版权'],
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/market/assets') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
assetId: 'asset-e2e',
|
||||||
|
name: '市场资产',
|
||||||
|
listingStatus: 'listed',
|
||||||
|
reviewStatus: 'approved',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/market/appeals') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
appealId: 'appeal-e2e',
|
||||||
|
assetId: 'asset-e2e',
|
||||||
|
appealType: 'delist',
|
||||||
|
appellantName: '申诉人',
|
||||||
|
assetName: '市场资产',
|
||||||
|
processorId: 'admin-e2e',
|
||||||
|
reason: '申诉材料',
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/account/users') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
status: 'active',
|
||||||
|
entitlementSource: 'plan',
|
||||||
|
nickname: 'E2E 用户',
|
||||||
|
newApiBindingStatus: 'bound',
|
||||||
|
quotaStatus: 'normal',
|
||||||
|
riskFlags: ['manual-review'],
|
||||||
|
userId: 'user-e2e',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/account/usage-records') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
attributionStatus: 'attributed',
|
||||||
|
nickname: 'E2E 用户',
|
||||||
|
period: '2026-05',
|
||||||
|
recordId: 'usage-e2e',
|
||||||
|
totalTokens: 42,
|
||||||
|
userId: 'user-e2e',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/account/purchase-records') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
amount: 0,
|
||||||
|
assetName: '市场资产',
|
||||||
|
assetType: 'agent',
|
||||||
|
createdAt: '2026-05-25T00:00:00Z',
|
||||||
|
purchaseType: 'admin_grant',
|
||||||
|
recordId: 'purchase-e2e',
|
||||||
|
status: 'completed',
|
||||||
|
userId: 'user-e2e',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/account/new-api-bindings') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
bindingStatus: 'bound',
|
||||||
|
externalUserId: 'newapi-user-e2e',
|
||||||
|
syncStatus: 'synced',
|
||||||
|
userId: 'user-e2e',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes('/account/users/') && path.endsWith('/balance-snapshots')) {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([{ balance: 100, snapshotId: 'balance-e2e', userId: 'user-e2e' }]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/jobs') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
createdAt: '2026-05-25T00:00:00Z',
|
||||||
|
createdBy: 'system',
|
||||||
|
jobId: 1001,
|
||||||
|
owner: 'market',
|
||||||
|
retryCount: 1,
|
||||||
|
status: 'failed',
|
||||||
|
type: 'source-projection',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/source-events') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
eventId: 2001,
|
||||||
|
actionPolicy: 'needs_recheck',
|
||||||
|
affectedScope: 'asset-e2e',
|
||||||
|
createdAt: '2026-05-25T00:00:00Z',
|
||||||
|
sourceId: 1,
|
||||||
|
sourceStatus: 'revoked',
|
||||||
|
sourceType: 'market_asset',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/audit/api-logs') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
actor: 'admin',
|
||||||
|
createdAt: '2026-05-25T00:00:00Z',
|
||||||
|
durationMs: 12,
|
||||||
|
logId: 1,
|
||||||
|
method: 'GET',
|
||||||
|
module: 'audit',
|
||||||
|
pathSummary: '/admin-api/muse/audit/api-logs',
|
||||||
|
requestId: 'req-e2e',
|
||||||
|
status: 'success',
|
||||||
|
statusCode: 200,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/audit/business-events') {
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: commonResult(
|
||||||
|
pageResult([
|
||||||
|
{
|
||||||
|
createdAt: '2026-05-25T00:00:00Z',
|
||||||
|
eventId: 1,
|
||||||
|
eventType: 'market.delist',
|
||||||
|
operator: 'admin',
|
||||||
|
resourceId: 'asset-e2e',
|
||||||
|
resourceType: 'market_asset',
|
||||||
|
summary: '市场资产下架',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unhandledMuseAdminApiPaths.add(path);
|
||||||
|
return route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
json: {
|
||||||
|
code: 500,
|
||||||
|
data: null,
|
||||||
|
msg: `Unhandled Muse admin E2E mock: ${path}`,
|
||||||
|
},
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.addInitScript((accessStoreKey) => {
|
await page.addInitScript((accessStoreKey) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@ -50,34 +511,94 @@ test.beforeEach(async ({ page }) => {
|
|||||||
menus: [
|
menus: [
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
component: 'muse/governance/meta-schema/index',
|
||||||
|
componentName: 'MuseMetaSchemaList',
|
||||||
|
id: 101,
|
||||||
|
name: '元结构定义',
|
||||||
|
parentId: 100,
|
||||||
|
path: 'governance/meta-schemas',
|
||||||
|
sort: 10,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'muse/governance/function-orch/index',
|
||||||
|
componentName: 'MuseFunctionOrchestration',
|
||||||
|
id: 102,
|
||||||
|
name: '功能编排',
|
||||||
|
parentId: 100,
|
||||||
|
path: 'governance/function-orchestration',
|
||||||
|
sort: 20,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'muse/ai/index',
|
||||||
|
componentName: 'MuseAiConfig',
|
||||||
|
id: 103,
|
||||||
|
name: 'AI 配置',
|
||||||
|
parentId: 100,
|
||||||
|
path: 'ai',
|
||||||
|
sort: 30,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'muse/knowledge/index',
|
||||||
|
componentName: 'MuseGlobalKnowledge',
|
||||||
|
id: 104,
|
||||||
|
name: '全局知识库',
|
||||||
|
parentId: 100,
|
||||||
|
path: 'knowledge',
|
||||||
|
sort: 40,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'muse/market/index',
|
||||||
|
componentName: 'MuseMarketGovernance',
|
||||||
|
id: 105,
|
||||||
|
name: '市场治理',
|
||||||
|
parentId: 100,
|
||||||
|
path: 'market',
|
||||||
|
sort: 50,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'muse/account/index',
|
component: 'muse/account/index',
|
||||||
componentName: 'MuseAccountGovernance',
|
componentName: 'MuseAccountGovernance',
|
||||||
id: 101,
|
id: 106,
|
||||||
name: '用户与权限',
|
name: '用户与权限',
|
||||||
parentId: 100,
|
parentId: 100,
|
||||||
path: 'account',
|
path: 'account',
|
||||||
sort: 10,
|
sort: 60,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'muse/newapi/index',
|
||||||
|
componentName: 'MuseNewApiUsage',
|
||||||
|
id: 107,
|
||||||
|
name: 'New-API 与用量',
|
||||||
|
parentId: 100,
|
||||||
|
path: 'new-api',
|
||||||
|
sort: 70,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'muse/jobs/index',
|
component: 'muse/jobs/index',
|
||||||
componentName: 'MuseTaskOps',
|
componentName: 'MuseTaskOps',
|
||||||
id: 102,
|
id: 108,
|
||||||
name: '任务治理',
|
name: '任务治理',
|
||||||
parentId: 100,
|
parentId: 100,
|
||||||
path: 'jobs',
|
path: 'jobs',
|
||||||
sort: 20,
|
sort: 80,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'muse/audit/index',
|
component: 'muse/audit/index',
|
||||||
componentName: 'MuseAuditLog',
|
componentName: 'MuseAuditLog',
|
||||||
id: 103,
|
id: 109,
|
||||||
name: '日志与审计',
|
name: '日志与审计',
|
||||||
parentId: 100,
|
parentId: 100,
|
||||||
path: 'audit',
|
path: 'audit',
|
||||||
sort: 30,
|
sort: 90,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -133,110 +654,7 @@ test.beforeEach(async ({ page }) => {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.route('**/admin-api/muse/account/users**', (route) =>
|
await routeMuseAdminApis(page);
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
accountStatus: 'active',
|
|
||||||
entitlementSource: 'plan',
|
|
||||||
nickname: 'E2E 用户',
|
|
||||||
quotaStatus: 'normal',
|
|
||||||
userId: 'user-e2e',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await page.route('**/admin-api/muse/account/usage-records**', (route) =>
|
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [{ recordId: 'usage-e2e', totalTokens: 42, userId: 'user-e2e' }],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await page.route('**/admin-api/muse/account/purchase-records**', (route) =>
|
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [{ orderId: 'order-e2e', status: 'paid', userId: 'user-e2e' }],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.route('**/admin-api/muse/jobs**', (route) =>
|
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
jobId: 'job-e2e',
|
|
||||||
jobType: 'source-projection',
|
|
||||||
owner: 'market',
|
|
||||||
status: 'failed',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await page.route('**/admin-api/muse/source-events**', (route) =>
|
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
eventId: 'event-e2e',
|
|
||||||
objectSummary: 'asset-e2e',
|
|
||||||
sourceStatus: 'revoked',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.route('**/admin-api/muse/audit/api-logs**', (route) =>
|
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
actor: 'admin',
|
|
||||||
durationMs: 12,
|
|
||||||
logId: 1,
|
|
||||||
method: 'GET',
|
|
||||||
pathSummary: '/admin-api/muse/audit/api-logs',
|
|
||||||
requestId: 'req-e2e',
|
|
||||||
status: 'success',
|
|
||||||
statusCode: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await page.route('**/admin-api/muse/audit/business-events**', (route) =>
|
|
||||||
route.fulfill({
|
|
||||||
contentType: 'application/json',
|
|
||||||
json: commonResult({
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
eventId: 1,
|
|
||||||
eventType: 'market.delist',
|
|
||||||
objectSummary: 'asset-e2e',
|
|
||||||
operator: 'admin',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('管理员可进入账号、任务和审计治理页面并看到边界文案', async ({
|
test('管理员可进入账号、任务和审计治理页面并看到边界文案', async ({
|
||||||
@ -246,13 +664,73 @@ test('管理员可进入账号、任务和审计治理页面并看到边界文
|
|||||||
await expect(page.getByText('用户、角色与权限治理')).toBeVisible();
|
await expect(page.getByText('用户、角色与权限治理')).toBeVisible();
|
||||||
await expect(page.getByText('不提供用户侧完整个人中心或私有正文入口')).toBeVisible();
|
await expect(page.getByText('不提供用户侧完整个人中心或私有正文入口')).toBeVisible();
|
||||||
await expect(page.getByText('E2E 用户')).toBeVisible();
|
await expect(page.getByText('E2E 用户')).toBeVisible();
|
||||||
|
await expect(page.getByText('manual-review')).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '用量与购买摘要' }).click();
|
||||||
|
await expect(page.getByText('usage-e2e')).toBeVisible();
|
||||||
|
await expect(page.getByText('purchase-e2e')).toBeVisible();
|
||||||
|
await expect(page.getByText('2026-05-25T00:00:00Z')).toBeVisible();
|
||||||
|
|
||||||
await page.goto('/muse/jobs');
|
await page.goto('/muse/jobs');
|
||||||
await expect(page.getByText('管理员不能代用户重新生成写作候选')).toBeVisible();
|
await expect(page.getByText('管理员不能代用户重新生成写作候选')).toBeVisible();
|
||||||
await expect(page.getByText('job-e2e')).toBeVisible();
|
await expect(page.getByText('1001')).toBeVisible();
|
||||||
|
await expect(page.getByText('source-projection')).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '来源事件' }).click();
|
||||||
|
await expect(page.getByText('2001')).toBeVisible();
|
||||||
|
await expect(page.getByText('market_asset')).toBeVisible();
|
||||||
|
await expect(page.getByText('needs_recheck')).toBeVisible();
|
||||||
|
|
||||||
await page.goto('/muse/audit');
|
await page.goto('/muse/audit');
|
||||||
await expect(page.getByRole('tab', { name: '接口调用日志' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: '接口调用日志' })).toBeVisible();
|
||||||
await expect(page.getByRole('tab', { name: '业务审计日志' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: '业务审计日志' })).toBeVisible();
|
||||||
await expect(page.getByText('req-e2e')).toBeVisible();
|
await expect(page.getByText('req-e2e')).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '业务审计日志' }).click();
|
||||||
|
await expect(page.getByText('市场资产下架')).toBeVisible();
|
||||||
|
|
||||||
|
expectNoUnhandledMuseAdminApis(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('管理员可进入核心治理配置页面并看到系统边界', async ({ page }) => {
|
||||||
|
await page.goto('/muse/governance/meta-schemas');
|
||||||
|
await expect(page.getByText('元结构定义').first()).toBeVisible();
|
||||||
|
await expect(page.getByText('story.draft')).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/muse/governance/function-orchestration');
|
||||||
|
await expect(
|
||||||
|
page.getByText('开放槽位只表示可替换的非保护节点').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText('故事链路')).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: '保护节点' }).first().click();
|
||||||
|
await expect(page.getByText('事实校验')).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/muse/ai');
|
||||||
|
await expect(page.getByText('AI 配置域治理边界')).toBeVisible();
|
||||||
|
await expect(page.getByText('候选生成 Prompt')).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '系统智能体' }).click();
|
||||||
|
await expect(page.getByText('用户私有 Agent 边界')).toBeVisible();
|
||||||
|
await expect(page.getByText('系统故事智能体')).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/muse/knowledge');
|
||||||
|
await expect(page.getByRole('heading', { name: '全局知识管理' })).toBeVisible();
|
||||||
|
await expect(page.getByText('不读取用户私有正文')).toBeVisible();
|
||||||
|
await expect(page.getByText('系统世界观库').first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/muse/market');
|
||||||
|
await expect(page.getByRole('heading', { name: '市场治理' })).toBeVisible();
|
||||||
|
await expect(page.getByText('待审资产').first()).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||||
|
await expect(page.getByText('不改用户私有副本').first()).toBeVisible();
|
||||||
|
await expect(page.getByText('市场资产').first()).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '申诉处理' }).click();
|
||||||
|
await expect(page.getByText('申诉人').first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/muse/new-api');
|
||||||
|
await expect(
|
||||||
|
page.getByText('New-API 页面只展示治理和对账摘要'),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText('不展示凭据')).toBeVisible();
|
||||||
|
await expect(page.getByRole('cell', { name: 'user-e2e' })).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: '调用归属' }).click();
|
||||||
|
await expect(page.getByText('usage-e2e')).toBeVisible();
|
||||||
|
|
||||||
|
expectNoUnhandledMuseAdminApis(page);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,11 +21,11 @@ describe('muse admin api client', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('为所有管理端 GET 请求追加 Muse 前缀和版本头', async () => {
|
it('为所有管理端 GET 请求追加 Muse 资源前缀和版本头', async () => {
|
||||||
await museAdminApi.get('/audit/api-logs', { pageNo: 1, pageSize: 20 });
|
await museAdminApi.get('/audit/api-logs', { pageNo: 1, pageSize: 20 });
|
||||||
|
|
||||||
expect(requestClientMock.get).toHaveBeenCalledWith(
|
expect(requestClientMock.get).toHaveBeenCalledWith(
|
||||||
'/admin-api/muse/audit/api-logs',
|
'/muse/audit/api-logs',
|
||||||
{
|
{
|
||||||
headers: { 'X-API-Version': '1' },
|
headers: { 'X-API-Version': '1' },
|
||||||
params: { pageNo: 1, pageSize: 20 },
|
params: { pageNo: 1, pageSize: 20 },
|
||||||
@ -41,12 +41,12 @@ describe('muse admin api client', () => {
|
|||||||
await museAdminApi.delete('/knowledge/global-kbs/kb-1/documents/doc-1');
|
await museAdminApi.delete('/knowledge/global-kbs/kb-1/documents/doc-1');
|
||||||
|
|
||||||
expect(requestClientMock.post).toHaveBeenCalledWith(
|
expect(requestClientMock.post).toHaveBeenCalledWith(
|
||||||
'/admin-api/muse/jobs/1/retry',
|
'/muse/jobs/1/retry',
|
||||||
body,
|
body,
|
||||||
{ headers: { 'X-API-Version': '1' } },
|
{ headers: { 'X-API-Version': '1' } },
|
||||||
);
|
);
|
||||||
expect(requestClientMock.request).toHaveBeenCalledWith(
|
expect(requestClientMock.request).toHaveBeenCalledWith(
|
||||||
'/admin-api/muse/knowledge/global-kbs/kb-1',
|
'/muse/knowledge/global-kbs/kb-1',
|
||||||
{
|
{
|
||||||
data: body,
|
data: body,
|
||||||
headers: { 'X-API-Version': '1' },
|
headers: { 'X-API-Version': '1' },
|
||||||
@ -54,7 +54,7 @@ describe('muse admin api client', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(requestClientMock.delete).toHaveBeenCalledWith(
|
expect(requestClientMock.delete).toHaveBeenCalledWith(
|
||||||
'/admin-api/muse/knowledge/global-kbs/kb-1/documents/doc-1',
|
'/muse/knowledge/global-kbs/kb-1/documents/doc-1',
|
||||||
{
|
{
|
||||||
headers: { 'X-API-Version': '1' },
|
headers: { 'X-API-Version': '1' },
|
||||||
params: undefined,
|
params: undefined,
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import { requestClient } from '#/api/request';
|
|||||||
* Muse 管理端 API 客户端
|
* Muse 管理端 API 客户端
|
||||||
*
|
*
|
||||||
* 统一收口 `/admin-api/muse/**` 请求,避免页面层拼错前缀或遗漏版本头。
|
* 统一收口 `/admin-api/muse/**` 请求,避免页面层拼错前缀或遗漏版本头。
|
||||||
|
* Vben requestClient 的 baseURL 已经是 `/admin-api`,这里仅拼接资源段 `/muse/**`,
|
||||||
|
* 运行时真实请求才会落到 `/admin-api/muse/**`,避免出现 `/admin-api/admin-api/muse/**`。
|
||||||
* Vben 的 requestClient 已经按 `code/data/msg` 解包,这里返回值即为后端 data。
|
* Vben 的 requestClient 已经按 `code/data/msg` 解包,这里返回值即为后端 data。
|
||||||
*/
|
*/
|
||||||
const MUSE_ADMIN_BASE_URL = '/admin-api/muse';
|
const MUSE_ADMIN_RESOURCE_PREFIX = '/muse';
|
||||||
|
|
||||||
/** Muse API v1 强制版本头,所有管理端请求都必须显式携带。 */
|
/** Muse API v1 强制版本头,所有管理端请求都必须显式携带。 */
|
||||||
const MUSE_API_HEADERS = {
|
const MUSE_API_HEADERS = {
|
||||||
@ -21,7 +23,7 @@ type MuseRequestBody = unknown;
|
|||||||
|
|
||||||
/** 拼接 Muse 管理端路径,确保调用方不能越过 `/admin-api/muse` 边界。 */
|
/** 拼接 Muse 管理端路径,确保调用方不能越过 `/admin-api/muse` 边界。 */
|
||||||
function toMuseUrl(url: string) {
|
function toMuseUrl(url: string) {
|
||||||
return `${MUSE_ADMIN_BASE_URL}${url}`;
|
return `${MUSE_ADMIN_RESOURCE_PREFIX}${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const museAdminApi = {
|
export const museAdminApi = {
|
||||||
|
|||||||
@ -17,7 +17,27 @@ import { computed, reactive, ref } from 'vue';
|
|||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import {
|
||||||
|
Alert as AAlert,
|
||||||
|
Button as AButton,
|
||||||
|
Card as ACard,
|
||||||
|
Descriptions as ADescriptions,
|
||||||
|
DescriptionsItem as ADescriptionsItem,
|
||||||
|
Drawer as ADrawer,
|
||||||
|
Empty as AEmpty,
|
||||||
|
Form as AForm,
|
||||||
|
FormItem as AFormItem,
|
||||||
|
Input as AInput,
|
||||||
|
InputNumber as AInputNumber,
|
||||||
|
List as AList,
|
||||||
|
ListItem as AListItem,
|
||||||
|
Space as ASpace,
|
||||||
|
Table as ATable,
|
||||||
|
Tag as ATag,
|
||||||
|
Textarea as ATextarea,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -1,8 +1,33 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { createApp, nextTick } from 'vue';
|
import { createApp, defineComponent, h, nextTick } from 'vue';
|
||||||
|
|
||||||
import KnowledgeIndex from '../index.vue';
|
import KnowledgeIndex from '../index.vue';
|
||||||
|
|
||||||
|
function makeStub(name: string) {
|
||||||
|
return defineComponent({
|
||||||
|
name,
|
||||||
|
props: ['dataSource', 'description', 'message', 'title', 'value'],
|
||||||
|
setup(props, { slots }) {
|
||||||
|
return () =>
|
||||||
|
h('section', { 'data-stub': name }, [
|
||||||
|
props.title,
|
||||||
|
props.message,
|
||||||
|
props.description,
|
||||||
|
props.value,
|
||||||
|
Array.isArray(props.dataSource)
|
||||||
|
? JSON.stringify(props.dataSource)
|
||||||
|
: '',
|
||||||
|
slots.default?.(),
|
||||||
|
slots.extra?.(),
|
||||||
|
slots.bodyCell?.({
|
||||||
|
column: { dataIndex: 'status', key: 'status' },
|
||||||
|
record: {},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
vi.mock('@vben/common-ui', () => ({
|
vi.mock('@vben/common-ui', () => ({
|
||||||
Page: {
|
Page: {
|
||||||
name: 'Page',
|
name: 'Page',
|
||||||
@ -11,9 +36,31 @@ vi.mock('@vben/common-ui', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('ant-design-vue', () => ({
|
vi.mock('ant-design-vue', () => ({
|
||||||
|
Alert: makeStub('Alert'),
|
||||||
|
Button: makeStub('Button'),
|
||||||
|
Card: makeStub('Card'),
|
||||||
|
Col: makeStub('Col'),
|
||||||
|
Descriptions: makeStub('Descriptions'),
|
||||||
|
DescriptionsItem: makeStub('DescriptionsItem'),
|
||||||
|
Form: makeStub('Form'),
|
||||||
|
FormItem: makeStub('FormItem'),
|
||||||
|
Input: makeStub('Input'),
|
||||||
|
Modal: makeStub('Modal'),
|
||||||
|
Row: makeStub('Row'),
|
||||||
|
Select: makeStub('Select'),
|
||||||
|
SelectOption: makeStub('SelectOption'),
|
||||||
|
Space: makeStub('Space'),
|
||||||
|
Statistic: makeStub('Statistic'),
|
||||||
|
Table: makeStub('Table'),
|
||||||
|
TabPane: makeStub('TabPane'),
|
||||||
|
Tabs: makeStub('Tabs'),
|
||||||
|
Tag: makeStub('Tag'),
|
||||||
|
Textarea: makeStub('Textarea'),
|
||||||
|
TypographyParagraph: makeStub('TypographyParagraph'),
|
||||||
message: {
|
message: {
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
|
warning: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,30 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
|||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import {
|
||||||
|
Alert as AAlert,
|
||||||
|
Button as AButton,
|
||||||
|
Card as ACard,
|
||||||
|
Col as ACol,
|
||||||
|
Descriptions as ADescriptions,
|
||||||
|
DescriptionsItem as ADescriptionsItem,
|
||||||
|
Form as AForm,
|
||||||
|
FormItem as AFormItem,
|
||||||
|
Input as AInput,
|
||||||
|
Modal as AModal,
|
||||||
|
Row as ARow,
|
||||||
|
Select as ASelect,
|
||||||
|
SelectOption as ASelectOption,
|
||||||
|
Space as ASpace,
|
||||||
|
Statistic as AStatistic,
|
||||||
|
Table as ATable,
|
||||||
|
TabPane as ATabPane,
|
||||||
|
Tabs as ATabs,
|
||||||
|
Tag as ATag,
|
||||||
|
Textarea as ATextarea,
|
||||||
|
TypographyParagraph as ATypographyParagraph,
|
||||||
|
message,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
disableGlobalKnowledgeBaseApi,
|
disableGlobalKnowledgeBaseApi,
|
||||||
|
|||||||
@ -1,8 +1,33 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { createApp, nextTick } from 'vue';
|
import { createApp, defineComponent, h, nextTick } from 'vue';
|
||||||
|
|
||||||
import MarketIndex from '../index.vue';
|
import MarketIndex from '../index.vue';
|
||||||
|
|
||||||
|
function makeStub(name: string) {
|
||||||
|
return defineComponent({
|
||||||
|
name,
|
||||||
|
props: ['dataSource', 'description', 'message', 'title', 'value'],
|
||||||
|
setup(props, { slots }) {
|
||||||
|
return () =>
|
||||||
|
h('section', { 'data-stub': name }, [
|
||||||
|
props.title,
|
||||||
|
props.message,
|
||||||
|
props.description,
|
||||||
|
props.value,
|
||||||
|
Array.isArray(props.dataSource)
|
||||||
|
? JSON.stringify(props.dataSource)
|
||||||
|
: '',
|
||||||
|
slots.default?.(),
|
||||||
|
slots.extra?.(),
|
||||||
|
slots.bodyCell?.({
|
||||||
|
column: { dataIndex: 'status', key: 'status' },
|
||||||
|
record: {},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
vi.mock('@vben/common-ui', () => ({
|
vi.mock('@vben/common-ui', () => ({
|
||||||
Page: {
|
Page: {
|
||||||
name: 'Page',
|
name: 'Page',
|
||||||
@ -11,9 +36,31 @@ vi.mock('@vben/common-ui', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('ant-design-vue', () => ({
|
vi.mock('ant-design-vue', () => ({
|
||||||
|
Alert: makeStub('Alert'),
|
||||||
|
Button: makeStub('Button'),
|
||||||
|
Card: makeStub('Card'),
|
||||||
|
Col: makeStub('Col'),
|
||||||
|
Descriptions: makeStub('Descriptions'),
|
||||||
|
DescriptionsItem: makeStub('DescriptionsItem'),
|
||||||
|
Form: makeStub('Form'),
|
||||||
|
FormItem: makeStub('FormItem'),
|
||||||
|
Input: makeStub('Input'),
|
||||||
|
Modal: makeStub('Modal'),
|
||||||
|
Row: makeStub('Row'),
|
||||||
|
Select: makeStub('Select'),
|
||||||
|
SelectOption: makeStub('SelectOption'),
|
||||||
|
Space: makeStub('Space'),
|
||||||
|
Statistic: makeStub('Statistic'),
|
||||||
|
Table: makeStub('Table'),
|
||||||
|
TabPane: makeStub('TabPane'),
|
||||||
|
Tabs: makeStub('Tabs'),
|
||||||
|
Tag: makeStub('Tag'),
|
||||||
|
Textarea: makeStub('Textarea'),
|
||||||
|
TypographyParagraph: makeStub('TypographyParagraph'),
|
||||||
message: {
|
message: {
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
|
warning: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,30 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
|||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import {
|
||||||
|
Alert as AAlert,
|
||||||
|
Button as AButton,
|
||||||
|
Card as ACard,
|
||||||
|
Col as ACol,
|
||||||
|
Descriptions as ADescriptions,
|
||||||
|
DescriptionsItem as ADescriptionsItem,
|
||||||
|
Form as AForm,
|
||||||
|
FormItem as AFormItem,
|
||||||
|
Input as AInput,
|
||||||
|
Modal as AModal,
|
||||||
|
Row as ARow,
|
||||||
|
Select as ASelect,
|
||||||
|
SelectOption as ASelectOption,
|
||||||
|
Space as ASpace,
|
||||||
|
Statistic as AStatistic,
|
||||||
|
Table as ATable,
|
||||||
|
TabPane as ATabPane,
|
||||||
|
Tabs as ATabs,
|
||||||
|
Tag as ATag,
|
||||||
|
Textarea as ATextarea,
|
||||||
|
TypographyParagraph as ATypographyParagraph,
|
||||||
|
message,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
approveMarketPublishRequestApi,
|
approveMarketPublishRequestApi,
|
||||||
@ -470,13 +493,26 @@ onMounted(loadMarketDashboard);
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.dataIndex === 'actions'">
|
<template v-else-if="column.dataIndex === 'actions'">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button size="small" @click="openReviewAction('approve', record)">
|
<a-button
|
||||||
|
size="small"
|
||||||
|
@click="
|
||||||
|
openReviewAction(
|
||||||
|
'approve',
|
||||||
|
record as AdminPublishRequestSummaryVO,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
通过
|
通过
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
danger
|
danger
|
||||||
size="small"
|
size="small"
|
||||||
@click="openReviewAction('reject', record)"
|
@click="
|
||||||
|
openReviewAction(
|
||||||
|
'reject',
|
||||||
|
record as AdminPublishRequestSummaryVO,
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
驳回
|
驳回
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -518,16 +554,37 @@ onMounted(loadMarketDashboard);
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.dataIndex === 'actions'">
|
<template v-else-if="column.dataIndex === 'actions'">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button size="small" @click="openAssetAction('preview', record)">
|
<a-button
|
||||||
|
size="small"
|
||||||
|
@click="
|
||||||
|
openAssetAction(
|
||||||
|
'preview',
|
||||||
|
record as AdminMarketAssetSummaryVO,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
影响预览
|
影响预览
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button size="small" @click="openAssetAction('delist', record)">
|
<a-button
|
||||||
|
size="small"
|
||||||
|
@click="
|
||||||
|
openAssetAction(
|
||||||
|
'delist',
|
||||||
|
record as AdminMarketAssetSummaryVO,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
下架
|
下架
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
danger
|
danger
|
||||||
size="small"
|
size="small"
|
||||||
@click="openAssetAction('recall', record)"
|
@click="
|
||||||
|
openAssetAction(
|
||||||
|
'recall',
|
||||||
|
record as AdminMarketAssetSummaryVO,
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
召回
|
召回
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -558,7 +615,10 @@ onMounted(loadMarketDashboard);
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.dataIndex === 'actions'">
|
<template v-else-if="column.dataIndex === 'actions'">
|
||||||
<a-button size="small" @click="openAppealAction(record)">
|
<a-button
|
||||||
|
size="small"
|
||||||
|
@click="openAppealAction(record as AdminAppealSummaryVO)"
|
||||||
|
>
|
||||||
处理
|
处理
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user