oh-my-muse/muse-studio/e2e/market-install-kb-retrieval.spec.ts
zizi 6dcabf69fb feat(solo): 收口治理入口并裁剪 studio 表面
完成 2.0.0 S4 的外部 AI、admin、account 与 solo SQL 收口,并推进 S5 studio 市场/handoff/旧编辑器入口裁剪。S5 的前端单测、构建与 route-only e2e 已留证;MSW-off 创作主线 e2e 仍因本地后端活体启动口径待修正,已在总账标为未完成验收。
2026-07-08 11:45:26 +08:00

147 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test, type Page } from '@playwright/test';
import { Client } from 'pg';
test.skip(true, '2.0.0 S5 单人版已移除市场、handoff 或多用户前端入口,本 spec 隔离保留待恢复');
/**
* D0-fork临时-04 U-verify市场知识库安装 → 物化 installed_ref → 检索打通 studio e2e真实后端MSW off
*
* ── 这个 spec 验什么、不验什么(诚实边界)──────────────────────────────────────────────
* D0-fork 端到端可观测面分两段:
* ①【安装侧物化,有 UI 面】用户安装市场知识库资产 → 绑定到作品 → 本地物化出 installed_ref KB + binding 指向
* 公开副本 dataset。这一段的最终可观测结果是「作品已绑定的知识来源」面板里出现该 market_kb 来源installed_ref
* 物化成功的用户可见证据)。本 spec 真打后端断言这一段。
* ②【检索命中 + 发布者私有不泄露,无独立 UI 面】retrieveForWork 只在 AI 生成编排链内部被调DefaultKnowledgeRetrievalFacade
* muse 没有「直接检索某作品知识库」的独立 HTTP/UI 入口;且「私有不泄露」是物理隔离断言,唯有真 RAGFlow 能证。
* 故②不在 studio e2e 证,而由真 PG + 真 RAGFlow IT 证:
* muse-cloud/muse-server/.../P1rMarketKbForkMaterializationIT已真跑绿publicChunksHit=3、privateMagicLeaked=false、
* d0ForkIsolationProven=true。在 e2e 重复②既无 UI 面可断言、又会被 AI 生成 SSE 慢路径拖成 flaky价值为负。
*
* ── 运行前提(待补,未跑则如实标)────────────────────────────────────────────────────
* 本 spec 要真跑需 global-setup 预置一份「fork 已就绪」的可安装 market_kb 资产:
* - listed 的 knowledge_base 市场资产 + 其公开副本 fork 已 forkStatus=readymuse_market_asset.tags 写入
* publicForkDatasetId/forkStatus=ready——这需要起全栈 muse-server + 真 RAGFlow 真 fork 一份副本(重活),
* 当前 global-setup#16 只 seed agent 资产、#6 只 seed 一条投影)尚未覆盖;
* - 安装者对该资产的有效 handoff tokenprecheck→bind 兑现链所需)。
* 在该前提就绪前,本 spec 标 test.fixme 跳过(不伪绿);前提补齐后去掉 fixme 即可真跑。
* 安装侧物化的后端落库语义已由 P1rMarketKbForkMaterializationIT 真证installerALocalKbId/installerBLocalKbId、
* 共享一份副本、幂等FE 安装/绑定面板渲染已由 market-install.spec.ts + knowledge-bindings.spec.ts 真证。
*
* 前置(与既有活体 e2e 同口径vite VITE_API_MOCK=false、真实 muse-server 48080、muse_slice_live
* PG 凭据从环境变量读MUSE_POSTGRES_*,见 ~/.config/muse-repo/infra.env绝不入库
* 跑前 `set -a; . ~/.config/muse-repo/infra.env; set +a`。
*/
const TOKEN = 'test1';
const API = 'http://localhost:48080/app-api/muse';
const AUTH = { Authorization: 'Bearer test1', 'tenant-id': '1', 'X-API-Version': '1' };
/** global-setup 待补fork 已就绪的可安装 market_kb 资产 marker 名(与 global-setup 约定,前提补齐时一并固化)。 */
const FORK_READY_KB_ASSET_NAME = '活体市场知识库·fork安装检索e2e';
function pgClient(): Client {
return new Client({
host: process.env.MUSE_POSTGRES_HOST,
port: Number(process.env.MUSE_POSTGRES_PORT ?? '5433'),
database: process.env.MUSE_POSTGRES_DATABASE ?? 'muse_slice_live',
user: process.env.MUSE_POSTGRES_USERNAME ?? 'root',
password: process.env.MUSE_POSTGRES_PASSWORD,
ssl: false,
});
}
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
window.localStorage.setItem('accessToken', t);
window.localStorage.setItem('tenantId', '1');
}, TOKEN);
}
/** 读 fork-ready market_kb 资产 id前提补齐后由 global-setup seed未 seed 则该断言会指引去补 global-setup。 */
async function readForkReadyAssetId(): Promise<number> {
const c = pgClient();
await c.connect();
try {
const r = await c.query(
"SELECT id FROM muse_market_asset WHERE tenant_id=1 AND name=$1 AND asset_type='knowledge_base' AND deleted=false",
[FORK_READY_KB_ASSET_NAME]
);
expect(
r.rowCount,
`需 global-setup 预置 fork-ready market_kb 资产「${FORK_READY_KB_ASSET_NAME}」(见本 spec 头部「运行前提」)`
).toBeGreaterThan(0);
return Number(r.rows[0].id);
} finally {
await c.end();
}
}
/** 读资产 tags 里的 forkStatusD0-fork仅 ready 才放行物化绑定)。 */
async function readForkStatus(assetId: number): Promise<string | null> {
const c = pgClient();
await c.connect();
try {
const r = await c.query(
"SELECT tags ->> 'forkStatus' AS fork_status FROM muse_market_asset WHERE tenant_id=1 AND id=$1",
[assetId]
);
return r.rowCount ? (r.rows[0].fork_status as string | null) : null;
} finally {
await c.end();
}
}
/** 读某作品下 installed_refsource_market_asset_id=assetId物化出的本地 KB 行数 + 其 binding 指向的 dataset。 */
async function readInstalledRefDataset(assetId: number, ownerUserId: number): Promise<string | null> {
const c = pgClient();
await c.connect();
try {
const r = await c.query(
`SELECT b.ragflow_dataset_id AS dataset_id
FROM muse_knowledge_base kb
JOIN muse_knowledge_ragflow_binding b
ON b.tenant_id = kb.tenant_id AND b.kb_id = kb.id
AND b.status = 'active' AND b.document_id IS NULL AND b.deleted = false
WHERE kb.tenant_id = 1 AND kb.kb_type = 'installed_ref'
AND kb.source_market_asset_id = $1 AND kb.owner_user_id = $2 AND kb.deleted = false
ORDER BY kb.id DESC LIMIT 1`,
[assetId, ownerUserId]
);
return r.rowCount ? (r.rows[0].dataset_id as string) : null;
} finally {
await c.end();
}
}
// 运行前提fork-ready 资产 + handoff token seed未就绪前标 fixme避免伪绿前提补齐后去掉 fixme 真跑。
test.fixme(
'D0-fork:安装 fork-ready 市场知识库 → 物化 installed_ref(binding 指向公开副本)→ 来源面板可见(真实后端)',
async ({ page, request }) => {
const assetId = await readForkReadyAssetId();
// 1) 前提核验:资产的公开副本 fork 必须 ready(D6 fail-closed:非 ready 不放行物化绑定)。
expect(await readForkStatus(assetId), 'D0-fork 安装前置:公开副本 forkStatus 必须 ready').toBe('ready');
// 2) 经真实安装 + 绑定链(purchase → handoff → precheck → bind)兑现物化(由 UI 或 page.request 真打,前提补齐时按
// market-handoff-precheck.spec.ts 同范式补全 token 兑现步骤)。
const acquire = await request.post(`${API}/marketplace/assets/${assetId}/purchase`, {
headers: AUTH,
data: { commandId: `e2e-fork-acq-${Date.now()}` },
});
expect(acquire.status(), 'purchase HTTP').toBe(200);
expect((await acquire.json()).code, 'purchase code').toBe(0);
// 3) 物化结果(DB 真证):安装者本地建出 installed_ref KB,其 dataset binding 指向公开副本(非发布者活库)。
const datasetId = await readInstalledRefDataset(assetId, 1);
expect(datasetId, '安装绑定后应物化出 installed_ref KB + 指向公开副本的 dataset binding').toBeTruthy();
// 4) 用户可见证据:进作品知识来源面板,看到该 market_kb 来源(installed_ref 物化成功的可观测结果)。
await seedToken(page);
await page.goto('/knowledge/1');
await expect(page.getByText('作品已绑定的知识来源').first()).toBeVisible({ timeout: 15_000 });
await expect(page.getByText('市场知识库').first()).toBeVisible({ timeout: 15_000 });
// 注:检索命中 + 发布者私有不泄露(②)不在此断言——无独立 UI 检索面,且物理隔离须真 RAGFlow,已由
// P1rMarketKbForkMaterializationIT 真证(privateMagicLeaked=false)。
}
);