oh-my-muse/muse-studio/e2e/knowledge-retrieval.spec.ts
lili 3585219637
Some checks failed
Backend Maven CI / backend-local (push) Has been cancelled
feat(mvp): 收束1.0.0线A交付闭环
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:52:10 -07:00

189 lines
8.0 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { Client } from 'pg';
/**
* E3 knowledge studio 检索与上传闭环 e2e(真实后端,MSW off)。
*
* 覆盖:
* 1. Studio 用户主动触发作品知识检索,真打 POST /works/{workId}/knowledge-retrievals;
* 2. 已绑定但无 active RAGFlow dataset 时,后端返回 no_dataset,UI 给出可治理提示;
* 3. 自建知识库资料上传从 UI 文件控件真打后端,并在 DB 落 document/processing task。
*
* 夹具由 global-setup.ts 用 E3 marker 每轮重建,避免污染业务种子或依赖固定自增 id。
*/
const TOKEN = 'test1';
const NO_DATASET_KB_NAME = 'E3 no_dataset e2e KB';
const UPLOAD_KB_NAME = 'E3 upload e2e KB';
const NO_DATASET_WORK_ID = 3;
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
window.localStorage.setItem('accessToken', t);
window.localStorage.setItem('tenantId', '1');
}, TOKEN);
}
function createPgClient(): 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,
connectionTimeoutMillis: 10_000,
});
}
async function readKnowledgeBaseIdByName(name: string): Promise<number> {
const client = createPgClient();
await client.connect();
try {
const result = await client.query(
'SELECT id FROM muse_knowledge_base WHERE tenant_id=1 AND name=$1 AND deleted=false ORDER BY id DESC LIMIT 1',
[name]
);
expect(result.rowCount, `应存在 e2e 知识库 fixture: ${name}`).toBe(1);
return Number(result.rows[0].id);
} finally {
await client.end();
}
}
async function readNoDatasetFixture(): Promise<{ kbId: number; bindingCount: number; projectionCount: number; datasetCount: number }> {
const client = createPgClient();
await client.connect();
try {
const result = await client.query(
`SELECT kb.id AS kb_id,
(SELECT count(*)::int FROM muse_knowledge_binding b
WHERE b.tenant_id=1 AND b.work_id=$2 AND b.kb_id=kb.id AND b.binding_status='active' AND b.deleted=false) AS binding_count,
(SELECT count(*)::int FROM muse_knowledge_source_binding_projection p
WHERE p.tenant_id=1 AND p.work_id=$2 AND p.kb_id=kb.id AND p.status='active' AND p.deleted=false) AS projection_count,
(SELECT count(*)::int FROM muse_knowledge_ragflow_binding rb
WHERE rb.tenant_id=1 AND rb.kb_id=kb.id AND rb.document_id IS NULL AND rb.status='active' AND rb.deleted=false) AS dataset_count
FROM muse_knowledge_base kb
WHERE kb.tenant_id=1 AND kb.name=$1 AND kb.deleted=false
ORDER BY kb.id DESC LIMIT 1`,
[NO_DATASET_KB_NAME, NO_DATASET_WORK_ID]
);
expect(result.rowCount, '应存在 E3 no_dataset fixture KB').toBe(1);
return {
kbId: Number(result.rows[0].kb_id),
bindingCount: Number(result.rows[0].binding_count),
projectionCount: Number(result.rows[0].projection_count),
datasetCount: Number(result.rows[0].dataset_count),
};
} finally {
await client.end();
}
}
async function readUploadedDocument(fileName: string): Promise<{
documentId: number;
kbId: number;
scanStatus: string;
processingStatus: string;
taskCount: number;
} | null> {
const client = createPgClient();
await client.connect();
try {
const result = await client.query(
`SELECT d.id AS document_id, d.kb_id, d.scan_status, v.processing_status,
(SELECT count(*)::int FROM muse_knowledge_processing_task t
WHERE t.tenant_id=1 AND t.document_id=d.id AND t.deleted=false) AS task_count
FROM muse_knowledge_document d
JOIN muse_knowledge_document_version v
ON v.tenant_id=d.tenant_id AND v.document_id=d.id AND v.deleted=false
WHERE d.tenant_id=1 AND d.file_name=$1 AND d.deleted=false
ORDER BY d.id DESC LIMIT 1`,
[fileName]
);
if (result.rowCount === 0) return null;
const row = result.rows[0];
return {
documentId: Number(row.document_id),
kbId: Number(row.kb_id),
scanStatus: String(row.scan_status),
processingStatus: String(row.processing_status),
taskCount: Number(row.task_count),
};
} finally {
await client.end();
}
}
test('主动检索已绑定知识来源:无 dataset 时 UI 显示治理提示(真实后端)', async ({ page }) => {
const fixture = await readNoDatasetFixture();
expect(fixture.bindingCount, 'no_dataset fixture 应有 active binding').toBeGreaterThan(0);
expect(fixture.projectionCount, 'no_dataset fixture 应有 active projection').toBeGreaterThan(0);
expect(fixture.datasetCount, 'no_dataset fixture 必须刻意没有 active dataset').toBe(0);
await seedToken(page);
await page.goto(`/knowledge/${NO_DATASET_WORK_ID}`);
await expect(page.getByRole('heading', { name: '检索作品知识' })).toBeVisible({ timeout: 15_000 });
const [resp] = await Promise.all([
page.waitForResponse(
(r) =>
new RegExp(`/works/${NO_DATASET_WORK_ID}/knowledge-retrievals$`).test(r.url()) &&
r.request().method() === 'POST',
{ timeout: 20_000 }
),
page.getByRole('button', { name: '检索' }).click(),
]);
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body.code, `检索响应: ${JSON.stringify(body)}`).toBe(0);
expect(body.data?.status).toBe('empty');
expect(body.data?.omittedReason).toBe('no_dataset');
expect((body.data?.omittedSources ?? []).some((source: { kbId?: string; reason?: string }) =>
source.kbId === String(fixture.kbId) && source.reason === 'no_dataset'
)).toBe(true);
await expect(page.getByText('已绑定的知识来源尚未生成可检索数据集。')).toBeVisible({ timeout: 15_000 });
await expect(page.getByText(/请进入下方“我创建的”知识库上传资料/)).toBeVisible({ timeout: 15_000 });
await expect(page.getByText(new RegExp(`KB ${fixture.kbId}\\(no_dataset\\)`))).toBeVisible({ timeout: 15_000 });
});
test('自建知识库资料上传主流程:Studio 文件上传真打后端并落库(真实后端)', async ({ page }) => {
const uploadKbId = await readKnowledgeBaseIdByName(UPLOAD_KB_NAME);
const fileName = `e3-upload-${Date.now()}.txt`;
await seedToken(page);
await page.goto('/knowledge');
await expect(page.getByText(UPLOAD_KB_NAME)).toBeVisible({ timeout: 15_000 });
await page.getByText(UPLOAD_KB_NAME).click();
await expect(page.getByText(`资料管理:${UPLOAD_KB_NAME}`)).toBeVisible({ timeout: 15_000 });
const [resp] = await Promise.all([
page.waitForResponse(
(r) => new RegExp(`/knowledge-bases/${uploadKbId}/documents$`).test(r.url()) && r.request().method() === 'POST',
{ timeout: 60_000 }
),
page.locator('input[type="file"]').setInputFiles({
name: fileName,
mimeType: 'text/plain',
buffer: Buffer.from(`E3 upload e2e fixture ${fileName}\n星环大陆知识检索上传主流程验证。`, 'utf-8'),
}),
]);
expect(resp.status(), '上传资料 HTTP 应为 201 Created 或 2xx').toBeGreaterThanOrEqual(200);
expect(resp.status(), '上传资料 HTTP 应为 201 Created 或 2xx').toBeLessThan(300);
const body = await resp.json();
expect(body.code, `上传响应: ${JSON.stringify(body).slice(0, 300)}`).toBe(0);
expect(body.data?.documentId).toBeTruthy();
expect(['pending_scan', 'processing', 'searchable', 'failed', 'blocked']).toContain(body.data?.processingStatus);
await expect(page.getByText(fileName)).toBeVisible({ timeout: 30_000 });
const dbRow = await readUploadedDocument(fileName);
expect(dbRow, '上传后应在 DB 找到 document/version/task 事实').not.toBeNull();
expect(dbRow!.kbId).toBe(uploadKbId);
expect(dbRow!.scanStatus).toMatch(/passed|pending|blocked|failed/);
expect(dbRow!.processingStatus).toBeTruthy();
expect(dbRow!.taskCount).toBeGreaterThan(0);
});