feat(knowledge): 展示文档处理进度并自动衔接导入索引
This commit is contained in:
@@ -13,6 +13,10 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
let draft: Record<string, unknown> | null = null;
|
||||
let recent: unknown = null;
|
||||
let knowledgeCatalogLoads = 0;
|
||||
let knowledgeCreated = false;
|
||||
let knowledgeUploaded = false;
|
||||
let knowledgeStage = -1;
|
||||
const knowledgeFile = { file_id: 'document', name: '本体(Ontology)学习路线图.pdf', size: 34000, error: null };
|
||||
let limits: { request_limit_points: string | null; daily_limit_points: string | null } = { request_limit_points: null, daily_limit_points: null };
|
||||
const jobs: Record<string, unknown>[] = [];
|
||||
const configuration = { model: '', tools: [], knowledges: [], mcps: [], skills: [], preload_skills: [], subagents: [], tool_approval_mode: 'default', max_execution_steps: 40, max_output_tokens: 4096, max_run_seconds: 600 };
|
||||
@@ -33,12 +37,28 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
});
|
||||
if (path.startsWith('/api/cloud-agents/')) {
|
||||
const body = typeof request.body === 'string' ? JSON.parse(request.body) : request.body ?? {};
|
||||
if (path.endsWith('/knowledge/pick')) {
|
||||
knowledgeUploaded = true;
|
||||
return result({ ...knowledgeFile, status: 'uploaded', chunk_count: 0, available: false, processing_task: null });
|
||||
}
|
||||
if (path.endsWith('/recovery')) return result({ recent, pending: [] });
|
||||
if (path.endsWith('/recovery/recent')) { recent = body; return result({ saved: true }); }
|
||||
if (path.endsWith('/actions')) {
|
||||
const input = body.input ?? {};
|
||||
if (body.operation === 'catalog') return result({ models: [{ id: 'test-model', name: '创作模型' }], resources: {}, pricing: null });
|
||||
if (body.operation === 'knowledge') return result({ databases: [], models: ++knowledgeCatalogLoads === 1 ? [] : [{ id: 'fixture:test-embedding', name: '资料向量模型', dimension: 1024 }] });
|
||||
if (body.operation === 'knowledge') return result({ databases: knowledgeCreated ? [{ kb_id: 'kb', name: '我的创作资料' }] : [], models: ++knowledgeCatalogLoads === 1 ? [] : [{ id: 'fixture:test-embedding', name: '资料向量模型', dimension: 1024 }] });
|
||||
if (body.operation === 'createKnowledge') { knowledgeCreated = true; return result({ kb_id: 'kb', name: '我的创作资料' }); }
|
||||
if (body.operation === 'processKnowledge') { knowledgeStage = 0; return result({ task_id: 'process-document' }); }
|
||||
if (body.operation === 'knowledgeFiles') {
|
||||
const stage = knowledgeStage;
|
||||
if (knowledgeStage >= 0) knowledgeStage++;
|
||||
return result({ next_offset: null, files: knowledgeUploaded ? [
|
||||
{ ...knowledgeFile, status: stage < 1 ? 'uploaded' : stage === 1 ? 'parsing' : stage === 2 ? 'indexing' : 'indexed',
|
||||
available: stage >= 3, chunk_count: stage >= 3 ? 18 : 0,
|
||||
processing_task: stage < 0 ? null : { task_id: 'process-document', status: stage === 0 ? 'pending' : stage < 3 ? 'running' : 'success', error: null } },
|
||||
{ file_id: 'empty', name: '扫描文档.pdf', size: 2000, status: 'indexed', available: false, chunk_count: 0, error: null },
|
||||
] : [] });
|
||||
}
|
||||
if (body.operation === 'resources') return result({ mcps: [], skills: [], subagents: [] });
|
||||
if (body.operation === 'budget' || body.operation === 'saveBudget') {
|
||||
if (body.operation === 'saveBudget') limits = input;
|
||||
@@ -117,10 +137,25 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
await expect(page.getByLabel('知识库名称', { exact: true })).toHaveValue('我的创作资料');
|
||||
await expect(page.getByRole('button', { name: '创建并选用', exact: true })).toBeEnabled();
|
||||
await expect(page.getByText('管理员配置说明', { exact: true })).toHaveCount(0);
|
||||
await page.getByLabel('知识库名称', { exact: true }).fill('');
|
||||
await page.getByRole('button', { name: '新建知识库', exact: true }).click();
|
||||
await page.getByRole('button', { name: '创建并选用', exact: true }).click();
|
||||
await expect(page.getByText('上传第一份文档,让智能体用你的资料回答。')).toBeVisible();
|
||||
await page.getByRole('button', { name: '上传文档', exact: true }).click();
|
||||
const document = page.getByRole('article', { name: '本体(Ontology)学习路线图.pdf' });
|
||||
await expect(document.getByText('等待处理', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('没有可用内容', { exact: true })).toBeVisible();
|
||||
await document.scrollIntoViewIfNeeded();
|
||||
await page.screenshot({ path: testInfo.outputPath('knowledge-queued.png') });
|
||||
await expect(document.getByText('正在解析文档', { exact: true })).toBeVisible();
|
||||
await expect(document.getByText('正在建立索引', { exact: true })).toBeVisible();
|
||||
await expect(document.getByText('可用于回答', { exact: true })).toBeVisible();
|
||||
await expect(document.getByText(/18 段可检索内容/)).toBeVisible();
|
||||
await document.scrollIntoViewIfNeeded();
|
||||
expect(await document.evaluate(el => el.scrollWidth - el.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await page.screenshot({ path: testInfo.outputPath('knowledge-ready.png') });
|
||||
await page.getByRole('button', { name: '我的要求', exact: true }).click();
|
||||
const preview = page.getByRole('region', { name: '草稿预览', exact: true });
|
||||
await preview.getByRole('button', { name: '保存并重新试用', exact: true }).click();
|
||||
await expect(preview.getByText('当前试用 · 修订 3')).toBeVisible();
|
||||
await preview.getByRole('button', { name: '你可以怎样帮助我?请举一个例子。', exact: true }).click();
|
||||
await expect(preview.getByLabel('消息')).toHaveValue('你可以怎样帮助我?请举一个例子。');
|
||||
await expect(preview.getByRole('heading', { name: '创作建议' })).toHaveCount(0);
|
||||
@@ -141,7 +176,7 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
await expect(page.getByText('云端已保存', { exact: true })).toBeVisible();
|
||||
await expect(preview.getByLabel('消息')).toHaveValue('保存后继续的问题');
|
||||
await expect(preview.getByRole('heading', { name: '创作建议' })).toBeAttached();
|
||||
await expect(preview.getByText('当前试用 · 修订 2')).toBeVisible();
|
||||
await expect(preview.getByText('当前试用 · 修订 3')).toBeVisible();
|
||||
const divider = page.getByRole('separator', { name: '调整配置与试用宽度' });
|
||||
await divider.focus();
|
||||
const previousRatio = Number(await divider.getAttribute('aria-valuenow'));
|
||||
@@ -175,14 +210,14 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
}
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].webContents.setZoomFactor(1));
|
||||
await preview.getByRole('button', { name: '用最新草稿试用' }).click();
|
||||
await expect(preview.getByText('当前试用 · 修订 3')).toBeVisible();
|
||||
await expect(preview.getByText('当前试用 · 修订 4')).toBeVisible();
|
||||
await expect(preview.getByLabel('消息')).toHaveValue('保存后继续的问题');
|
||||
await preview.getByText('对比上一次试用 · 修订 2', { exact: true }).click();
|
||||
await preview.getByText('对比上一次试用 · 修订 3', { exact: true }).click();
|
||||
await expect(preview.getByRole('heading', { name: '创作建议' })).toBeVisible();
|
||||
await preview.getByText('对比上一次试用 · 修订 2', { exact: true }).click();
|
||||
await preview.getByText('对比上一次试用 · 修订 3', { exact: true }).click();
|
||||
await page.getByRole('button', { name: '简单易懂', exact: true }).click();
|
||||
await preview.getByRole('button', { name: '保存并重新试用', exact: true }).click();
|
||||
await expect(preview.getByText('当前试用 · 修订 4')).toBeVisible();
|
||||
await expect(preview.getByText('当前试用 · 修订 5')).toBeVisible();
|
||||
await expect(preview.getByLabel('消息')).toHaveValue('保存后继续的问题');
|
||||
await preview.getByLabel('消息').fill('');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
|
||||
@@ -151,6 +151,28 @@ describe('Main cloud Agents boundary', () => {
|
||||
expect(transport).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves knowledge readiness and processing fields through Main upload and attachment import', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'makelore-knowledge-status-'));
|
||||
try {
|
||||
const source = join(directory, 'replacement.txt');
|
||||
await writeFile(source, 'knowledge content');
|
||||
desktop.pick.mockResolvedValue({ canceled: false, filePaths: [source] });
|
||||
const file = { file_id: 'new-file', name: 'replacement.txt', size: 17, status: 'indexed', error: null,
|
||||
chunk_count: 2, available: true, replaces_file_id: 'old-file',
|
||||
processing_task: { task_id: 'task', status: 'success', error: null } };
|
||||
const transport = vi.fn().mockResolvedValueOnce(session())
|
||||
.mockResolvedValueOnce(json({ ...file, access_token: 'never-render' }))
|
||||
.mockResolvedValueOnce(json({ ...file, access_token: 'never-render' }));
|
||||
const module = moduleFor(transport);
|
||||
const request = { slug: draft.slug, kb_id: 'kb_test', operation_id: input.operation_id };
|
||||
expect(await module.uploadKnowledge({ ...request, replaces_file_id: 'old-file' })).toEqual(file);
|
||||
expect(await module.execute({ operation: 'importKnowledgeAttachment', input: {
|
||||
...request, thread_id: 'thread', attachment_id: 'attachment',
|
||||
} })).toEqual(file);
|
||||
expect(new URL(transport.mock.calls[1][0]).searchParams.get('replaces_file_id')).toBe('old-file');
|
||||
} finally { await rm(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it('notifies only a new unread terminal state and resets its baseline on account switch', async () => {
|
||||
vi.useFakeTimers();
|
||||
const transport = vi.fn().mockResolvedValueOnce(session()).mockResolvedValueOnce(json({ agents: [], next_cursor: null }))
|
||||
|
||||
225
tests/unit/cloud-knowledge-status.test.tsx
Normal file
225
tests/unit/cloud-knowledge-status.test.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { CloudKnowledgePanel } from '@/pages/CloudAgents/CloudKnowledge';
|
||||
import type { CloudKnowledgeFile } from '../../shared/cloud-agents';
|
||||
|
||||
const api = vi.hoisted(() => ({ call: vi.fn(), uploadKnowledge: vi.fn() }));
|
||||
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
||||
const file: CloudKnowledgeFile = { file_id: 'file', name: '学习路线图.pdf', size: 3000, status: 'uploaded', chunk_count: 0, error: null, available: false };
|
||||
let items: CloudKnowledgeFile[];
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks(); items = [];
|
||||
api.call.mockImplementation(async operation => {
|
||||
if (operation === 'knowledge') return { databases: [{ kb_id: 'kb', name: '资料' }, { kb_id: 'other', name: '其他库' }], models: [] };
|
||||
if (operation === 'knowledgeFiles') return { files: items, next_offset: null };
|
||||
if (operation === 'processKnowledge') {
|
||||
items = items.map(value => ({ ...value, processing_task: { task_id: 'task', status: 'pending', error: null } }));
|
||||
return { task_id: 'task' };
|
||||
}
|
||||
throw new Error('unexpected ' + operation);
|
||||
});
|
||||
});
|
||||
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
||||
async function open() {
|
||||
render(<CloudKnowledgePanel slug="ml-agent" onCreated={() => undefined} />);
|
||||
fireEvent.click(screen.getByText('管理我的知识文档'));
|
||||
await screen.findByRole('option', { name: '资料' });
|
||||
fireEvent.change(screen.getByLabelText('管理的知识库'), { target: { value: 'kb' } });
|
||||
}
|
||||
async function tick() { await act(async () => { await vi.advanceTimersByTimeAsync(5000); }); }
|
||||
|
||||
it('uploads then processes automatically and refreshes through queue, parse, index and readiness', async () => {
|
||||
await open();
|
||||
await screen.findByText('上传第一份文档,让智能体用你的资料回答。');
|
||||
vi.useFakeTimers();
|
||||
api.uploadKnowledge.mockImplementation(async () => { items = [file]; return file; });
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '上传文档', exact: true })); });
|
||||
expect(screen.getByText('等待处理')).toBeVisible();
|
||||
expect(screen.queryByText(/0 个分块/)).not.toBeInTheDocument();
|
||||
expect(api.call).toHaveBeenCalledWith('processKnowledge', expect.objectContaining({ kb_id: 'kb', file_id: 'file' }));
|
||||
items = [{ ...file, status: 'parsing', processing_task: { task_id: 'task', status: 'running', error: null } }];
|
||||
await tick(); expect(screen.getByText('正在解析文档')).toBeVisible();
|
||||
items = [{ ...items[0], status: 'indexing' }];
|
||||
await tick(); expect(screen.getByText('正在建立索引')).toBeVisible();
|
||||
items = [{ ...file, status: 'indexed', available: true, chunk_count: 12, processing_task: { task_id: 'task', status: 'success', error: null } }];
|
||||
await tick();
|
||||
expect(screen.getByText('可用于回答')).toBeVisible();
|
||||
expect(screen.getByText(/12 段可检索内容/)).toBeVisible();
|
||||
expect(api.uploadKnowledge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not call zero chunks searchable and distinguishes loading from an empty library', async () => {
|
||||
let resolve!: (value: unknown) => void;
|
||||
const original = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation((operation, input) => operation === 'knowledgeFiles'
|
||||
? new Promise(value => { resolve = value; }) : original(operation, input));
|
||||
await open();
|
||||
expect(screen.getByText('正在读取文档状态…')).toBeVisible();
|
||||
expect(screen.queryByText(/上传第一份/)).not.toBeInTheDocument();
|
||||
await act(async () => resolve({ files: [{ ...file, status: 'indexed' }], next_offset: null }));
|
||||
expect(screen.getByText('没有可用内容')).toBeVisible();
|
||||
expect(screen.queryByText('可用于回答')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/扫描 PDF 需先识别文字/)).toBeVisible();
|
||||
});
|
||||
|
||||
it('retains the saved upload and processing identity after an uncertain submission', async () => {
|
||||
const original = api.call.getMockImplementation()!;
|
||||
const attempts: unknown[] = [];
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation === 'processKnowledge') {
|
||||
attempts.push(input);
|
||||
if (attempts.length === 1) throw new Error('连接超时');
|
||||
}
|
||||
return original(operation, input);
|
||||
});
|
||||
api.uploadKnowledge.mockImplementation(async () => { items = [file]; return file; });
|
||||
await open();
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传文档', exact: true }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('文件已保存,处理请求尚未确认');
|
||||
expect(screen.getByRole('article', { name: file.name })).toBeVisible();
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试处理' }));
|
||||
await screen.findByText('等待处理');
|
||||
expect(attempts).toHaveLength(2);
|
||||
expect(attempts[0]).toEqual(attempts[1]);
|
||||
expect(api.uploadKnowledge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('restores terminal task errors on entry and clears stale read errors after recovery', async () => {
|
||||
items = [{ ...file, processing_task: { task_id: 'task', status: 'failed', error: '文档读取失败' } }];
|
||||
await open();
|
||||
expect(await screen.findByText('处理失败')).toBeVisible();
|
||||
expect(screen.getByText('文档读取失败')).toBeVisible();
|
||||
vi.useFakeTimers();
|
||||
api.call.mockRejectedValueOnce(new Error('读取超时'));
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '刷新状态' })));
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('以下保留上次结果');
|
||||
expect(screen.getByRole('article')).toBeVisible();
|
||||
await tick();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(['success', 'failed'] as const)('uses a newly observed %s Task to resolve an uncertain submission', async status => {
|
||||
items = [{ ...file, processing_task: { task_id: 'old-task', status: 'failed', error: '上次失败' } }];
|
||||
const attempts: { operation_id: string }[] = [];
|
||||
const original = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation === 'processKnowledge') {
|
||||
attempts.push(input);
|
||||
if (attempts.length < 3) throw new Error('连接超时');
|
||||
}
|
||||
return original(operation, input);
|
||||
});
|
||||
await open();
|
||||
await screen.findByText('处理失败');
|
||||
vi.useFakeTimers();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '重试处理' })));
|
||||
await tick(); // 旧 Task 仍可见时,保留同一请求身份。
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('处理请求尚未确认');
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '重试处理' })));
|
||||
expect(attempts[1].operation_id).toBe(attempts[0].operation_id);
|
||||
items = [{ ...file, status: status === 'success' ? 'indexed' : 'uploaded',
|
||||
available: status === 'success', chunk_count: status === 'success' ? 3 : 0,
|
||||
processing_task: { task_id: 'new-task', status, error: status === 'failed' ? '新处理失败' : null } }];
|
||||
await tick();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
if (status === 'success') {
|
||||
expect(screen.getByText('可用于回答')).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: '重试处理' })).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.getByText('新处理失败')).toBeVisible();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '重试处理' })));
|
||||
expect(attempts[2].operation_id).not.toBe(attempts[0].operation_id);
|
||||
expect(screen.getByText('等待处理')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps all loaded pages refreshing and ignores a late response from the previous knowledge base', async () => {
|
||||
let late!: (value: unknown) => void;
|
||||
let useLate = false;
|
||||
const original = api.call.getMockImplementation()!;
|
||||
const second = { ...file, file_id: 'second', name: '第二页.txt' };
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation !== 'knowledgeFiles') return original(operation, input);
|
||||
if (input.kb_id === 'other') return { files: [], next_offset: null };
|
||||
if (useLate) return new Promise(resolve => { late = resolve; });
|
||||
return input.offset ? { files: [second], next_offset: null } : { files: [file], next_offset: 100 };
|
||||
});
|
||||
await open();
|
||||
const more = await screen.findByRole('button', { name: '加载更多文档' });
|
||||
vi.useFakeTimers();
|
||||
await act(async () => fireEvent.click(more));
|
||||
expect(screen.getByRole('article', { name: '第二页.txt' })).toBeVisible();
|
||||
second.status = 'indexed'; second.available = true; second.chunk_count = 6;
|
||||
await tick();
|
||||
expect(within(screen.getByRole('article', { name: '第二页.txt' })).getByText('可用于回答')).toBeVisible();
|
||||
useLate = true;
|
||||
await tick();
|
||||
await act(async () => fireEvent.change(screen.getByLabelText('管理的知识库'), { target: { value: 'other' } }));
|
||||
await act(async () => late({ files: [file], next_offset: null }));
|
||||
expect(screen.queryByRole('article')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('上传第一份文档,让智能体用你的资料回答。')).toBeVisible();
|
||||
});
|
||||
|
||||
it('imports an attachment and processes it without claiming immediate readiness', async () => {
|
||||
const original = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation === 'threads') return { threads: [{ thread_id: 'thread', title: '我的对话' }], next_offset: null };
|
||||
if (operation === 'attachments') return { attachments: [{ file_id: 'attachment', file_name: '附件.pdf' }] };
|
||||
if (operation === 'importKnowledgeAttachment') { items = [file]; return file; }
|
||||
return original(operation, input);
|
||||
});
|
||||
await open();
|
||||
fireEvent.click(screen.getByRole('button', { name: '从对话附件导入' }));
|
||||
await screen.findByRole('option', { name: '我的对话' });
|
||||
fireEvent.change(screen.getByLabelText('附件来源对话'), { target: { value: 'thread' } });
|
||||
await screen.findByRole('option', { name: '附件.pdf' });
|
||||
fireEvent.change(screen.getByLabelText('入库附件'), { target: { value: 'attachment' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认导入知识库' }));
|
||||
await screen.findByText('等待处理');
|
||||
expect(screen.queryByText('可用于回答')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps indexed content available when replacement cleanup fails and permits retry', async () => {
|
||||
items = [{ ...file, status: 'indexed', chunk_count: 4, available: true, replaces_file_id: 'old',
|
||||
processing_task: { task_id: 'task', status: 'failed', error: '旧文档清理失败' } }];
|
||||
await open();
|
||||
expect(await screen.findByText('内容已可用,处理未全部完成')).toBeVisible();
|
||||
expect(screen.getByText(/已显示 1 份文档 · 1 份可用/)).toBeVisible();
|
||||
expect(screen.getByText(/4 段可检索内容/)).toBeVisible();
|
||||
expect(screen.getByText(/旧文档清理失败/)).toBeVisible();
|
||||
vi.useFakeTimers();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '重试处理' })));
|
||||
expect(screen.getByText('内容已可用,正在完成处理')).toBeVisible();
|
||||
expect(screen.getByText(/已显示 1 份文档 · 1 份可用/)).toBeVisible();
|
||||
expect(screen.getByText(/4 段可检索内容/)).toBeVisible();
|
||||
items = [{ ...items[0], processing_task: { task_id: 'task', status: 'running', error: null } }];
|
||||
await tick();
|
||||
expect(screen.getByText('内容已可用,正在完成处理')).toBeVisible();
|
||||
expect(screen.getByText(/已显示 1 份文档 · 1 份可用/)).toBeVisible();
|
||||
expect(screen.getByText(/4 段可检索内容/)).toBeVisible();
|
||||
items = [{ ...items[0], processing_task: { task_id: 'task', status: 'success', error: null } }];
|
||||
await tick();
|
||||
expect(screen.getByText('可用于回答')).toBeVisible();
|
||||
expect(api.call).toHaveBeenCalledWith('processKnowledge', expect.objectContaining({ file_id: 'file' }));
|
||||
expect(api.uploadKnowledge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('automatically processes a replacement while retaining the usable old document on failure', async () => {
|
||||
items = [{ ...file, status: 'indexed', chunk_count: 4, available: true }];
|
||||
const replacement = { ...file, file_id: 'new', name: '新版.pdf', replaces_file_id: 'file' };
|
||||
api.uploadKnowledge.mockImplementation(async () => { items = [...items, replacement]; return replacement; });
|
||||
const original = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation === 'processKnowledge') {
|
||||
items = items.map(value => value.file_id === 'new' ? { ...replacement, status: 'error_parsing', processing_task: { task_id: 'task', status: 'failed', error: '文字读取失败' } } : value);
|
||||
return { task_id: 'task' };
|
||||
}
|
||||
return original(operation, input);
|
||||
});
|
||||
await open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: '替换文档' }));
|
||||
await screen.findByText('处理失败');
|
||||
expect(within(screen.getByRole('article', { name: file.name })).getByText('可用于回答')).toBeVisible();
|
||||
expect(screen.getByText(/这是替换文档/)).toBeVisible();
|
||||
expect(api.uploadKnowledge).toHaveBeenCalledWith('ml-agent', 'kb', expect.any(String), 'file');
|
||||
});
|
||||
Reference in New Issue
Block a user