完成 2.0.0 S4 的外部 AI、admin、account 与 solo SQL 收口,并推进 S5 studio 市场/handoff/旧编辑器入口裁剪。S5 的前端单测、构建与 route-only e2e 已留证;MSW-off 创作主线 e2e 仍因本地后端活体启动口径待修正,已在总账标为未完成验收。
147 lines
8.1 KiB
TypeScript
147 lines
8.1 KiB
TypeScript
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=ready(muse_market_asset.tags 写入
|
||
* publicForkDatasetId/forkStatus=ready)——这需要起全栈 muse-server + 真 RAGFlow 真 fork 一份副本(重活),
|
||
* 当前 global-setup(#16 只 seed agent 资产、#6 只 seed 一条投影)尚未覆盖;
|
||
* - 安装者对该资产的有效 handoff token(precheck→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 里的 forkStatus(D0-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_ref(source_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)。
|
||
}
|
||
);
|