Some checks failed
Backend Maven CI / backend-local (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
189 lines
8.0 KiB
TypeScript
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);
|
|
});
|