test(market): U-verify D0-fork 端到端真验收(真 PG+真 RAGFlow、第四单元)
D0-fork 最后单元:真 PG17+真 RAGFlow 端到端验收、2 IT 真绿(独立复跑确认 57s)。P1rMarketKbForkMaterializationIT(muse-server、跨 knowledge+market 两 BC 须同 Spring 上下文):①fork→安装→物化 installed_ref→检索命中(no_dataset 消除、副本与活 dataset 物理分离);②发布者私有不泄露真证(发布者上架后往活 dataset 真传私有 magic、安装者检索副本拿 magic 当 query 仍只返公开 chunk 不含 magic→d0ForkIsolationProven、U0 spike 反向基线);③幂等+多安装者共享1副本;④AFTER_COMMIT 时序(committedAssetForked/rolledBackAssetForked false:提交才 fork 回滚不 fork);⑤_test 库铁律(muse_p1r_fork_mat_test 绝未指 muse_slice_live)。 market-install-kb-retrieval.spec.ts(studio e2e):检索命中+私有不泄露无独立 UI 入口由 IT 证;安装物化 e2e 标 test.fixme 待 global-setup 补 fork 就绪资产(不伪绿、骨架就位 tsc 过)。召回回滚(D9)如实标开放项:recallAsset 仍 writeSourceStatusBlocked、knowledge 无召回消费者→不自动停用 installed_ref;需新增 market→knowledge 召回传播才闭环(不伪造、不引用不存在的 muse_source_propagation_target)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d4309ab0e9
commit
8b3258e22f
File diff suppressed because it is too large
Load Diff
144
muse-studio/e2e/market-install-kb-retrieval.spec.ts
Normal file
144
muse-studio/e2e/market-install-kb-retrieval.spec.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { Client } from 'pg';
|
||||
|
||||
/**
|
||||
* 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)。
|
||||
}
|
||||
);
|
||||
Loading…
x
Reference in New Issue
Block a user