346 lines
24 KiB
TypeScript
346 lines
24 KiB
TypeScript
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
import { CloudKnowledgeAttachmentPicker } from '@/pages/CloudAgents/CloudKnowledgeAttachmentPicker';
|
|
import { CloudKnowledgePanel } from '@/pages/CloudAgents/CloudKnowledge';
|
|
import type { CloudAttachment, CloudKnowledgeFile, CloudThread } from '../../shared/cloud-agents';
|
|
|
|
const api = vi.hoisted(() => ({ call: vi.fn(), list: vi.fn() }));
|
|
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
|
const slug = 'ml-' + 'a'.repeat(32), otherSlug = 'ml-' + 'b'.repeat(32);
|
|
const threads: CloudThread[] = [
|
|
{ thread_id: 'spring', title: '春天的观察讨论', agent_slug: slug, client_thread_id: null, updated_at: '2026-10-13T00:00:00Z', mode: 'published', run_id: null, status: 'completed', unread: false },
|
|
{ thread_id: 'reading', title: '图书阅读记录', agent_slug: otherSlug, client_thread_id: null, updated_at: '2026-10-12T00:00:00Z', mode: 'preview', run_id: null, status: 'completed', unread: false },
|
|
];
|
|
function attachment(id: string, name: string, size = 1536): CloudAttachment {
|
|
return { file_id: id, file_name: name, file_size: size, path: '/attachments/' + name, original_path: '/attachments/' + name };
|
|
}
|
|
function knowledgeFile(id: string): CloudKnowledgeFile {
|
|
return { file_id: 'knowledge-' + id, name: id + '.txt', size: 1536, status: 'uploaded', available: false, chunk_count: 0, error: null };
|
|
}
|
|
let sourceFiles: Record<string, CloudAttachment[]>;
|
|
const imported = vi.fn<(file: CloudKnowledgeFile) => Promise<void>>();
|
|
const pending = vi.fn();
|
|
const close = vi.fn();
|
|
const props = { slug, kbId: 'kb_own', kbName: '我的观察记录', busy: false, act: async (task: () => Promise<void>) => task(), onImported: imported, onPendingChange: pending, onClose: close };
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks(); imported.mockResolvedValue();
|
|
sourceFiles = { spring: [attachment('a', '观察报告.md'), attachment('b', '植物资料.pdf')], reading: [attachment('a', '阅读笔记.txt')] };
|
|
api.list.mockResolvedValue({ agents: [{ slug, name: '资料小助手' }], next_cursor: null });
|
|
api.call.mockImplementation(async (operation: string, input: { thread_id?: string; attachment_id?: string }) => {
|
|
if (operation === 'threads') return { threads, next_offset: null };
|
|
if (operation === 'received') return { agents: [{ slug: otherSlug, name: '阅读搭档' }] };
|
|
if (operation === 'attachments') return { attachments: sourceFiles[input.thread_id!] };
|
|
if (operation === 'importKnowledgeAttachment') return knowledgeFile(input.attachment_id!);
|
|
throw new Error('Unexpected operation ' + operation);
|
|
});
|
|
});
|
|
afterEach(cleanup);
|
|
async function chooseChat(title = threads[0].title) {
|
|
fireEvent.click(await screen.findByRole('button', { name: '选择对话 ' + title }));
|
|
}
|
|
async function chooseFile(name: string) { fireEvent.click(await screen.findByRole('checkbox', { name: '选择文件 ' + name })); }
|
|
function imports() { return api.call.mock.calls.filter(([operation]) => operation === 'importKnowledgeAttachment').map(([, input]) => input); }
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (error: Error) => void;
|
|
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
it('searches named and dated chats, keeps selections across chats, and imports only explicitly selected attachments', async () => {
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'threads'
|
|
? Promise.resolve(input.offset ? { threads: [threads[1]], next_offset: null } : { threads: [threads[0]], next_offset: 50 }) : original(operation, input));
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
expect(screen.getByRole('dialog', { name: '从聊天选择文件' })).toHaveTextContent('导入到「我的观察记录」');
|
|
await screen.findByText(/阅读搭档 · 2026/);
|
|
fireEvent.change(screen.getByRole('textbox', { name: '搜索对话' }), { target: { value: '资料小助手' } });
|
|
expect(screen.queryByRole('button', { name: '选择对话 图书阅读记录' })).not.toBeInTheDocument();
|
|
await chooseChat();
|
|
expect(await screen.findByText('MD · 2 KB')).toBeVisible();
|
|
fireEvent.change(screen.getByRole('textbox', { name: '搜索文件' }), { target: { value: '观察' } });
|
|
expect(screen.queryByRole('checkbox', { name: '选择文件 植物资料.pdf' })).not.toBeInTheDocument();
|
|
await chooseFile('观察报告.md');
|
|
fireEvent.change(screen.getByRole('textbox', { name: '搜索对话' }), { target: { value: '阅读' } });
|
|
await chooseChat(threads[1].title); await chooseFile('阅读笔记.txt');
|
|
expect(within(screen.getByRole('list', { name: '已选聊天文件' })).getAllByRole('listitem')).toHaveLength(2);
|
|
expect(imports()).toHaveLength(0);
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' }));
|
|
await waitFor(() => expect(close).toHaveBeenCalledTimes(1));
|
|
expect(imports()).toEqual([
|
|
expect.objectContaining({ slug, kb_id: 'kb_own', thread_id: 'spring', attachment_id: 'a' }),
|
|
expect.objectContaining({ slug, kb_id: 'kb_own', thread_id: 'reading', attachment_id: 'a' }),
|
|
]);
|
|
expect(new Set(imports().map(input => input.operation_id)).size).toBe(2);
|
|
expect(imported).toHaveBeenCalledTimes(2);
|
|
expect(api.call.mock.calls.some(([operation]) => ['history', 'files', 'deleteAttachment'].includes(operation))).toBe(false);
|
|
expect(pending).toHaveBeenLastCalledWith(false);
|
|
});
|
|
|
|
it('retries only failed files with their original identity, retaining successful files and processing callbacks', async () => {
|
|
const original = api.call.getMockImplementation()!;
|
|
let failure = true;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' && input.attachment_id === 'b' && failure
|
|
? Promise.reject(new Error('连接超时')) : original(operation, input));
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md'); await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' }));
|
|
await screen.findByText(/导入结果尚未确认:连接超时/);
|
|
expect(close).not.toHaveBeenCalled(); expect(imported).toHaveBeenCalledTimes(1);
|
|
expect(screen.getByRole('button', { name: '关闭文件选择' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: '选择对话 图书阅读记录' })).toBeDisabled();
|
|
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
|
|
expect(close).not.toHaveBeenCalled();
|
|
const first = imports();
|
|
failure = false;
|
|
fireEvent.click(screen.getByRole('button', { name: '重试失败的 1 个文件' }));
|
|
await waitFor(() => expect(close).toHaveBeenCalledTimes(1));
|
|
expect(imports()).toHaveLength(3); expect(imports()[2]).toEqual(first[1]);
|
|
expect(imported).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('never reimports a confirmed file after a processing callback failure and permits a later independent batch', async () => {
|
|
imported.mockRejectedValueOnce(new Error('处理失败'));
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 1 个文件' }));
|
|
await screen.findByText(/文件已导入,处理未全部启动/);
|
|
expect(imports()).toHaveLength(1); expect(close).not.toHaveBeenCalled();
|
|
expect(screen.getByRole('button', { name: '关闭文件选择' })).toBeEnabled();
|
|
await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 1 个文件' }));
|
|
await waitFor(() => expect(close).toHaveBeenCalledTimes(1));
|
|
expect(imports().map(input => input.attachment_id)).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('shows file types and sizes, disables unsupported legacy formats and oversize files, and accepts supported files at the size boundary', async () => {
|
|
sourceFiles.spring = [attachment('legacy-doc', '旧文档.doc'), attachment('legacy-xls', '旧表格.xls'), attachment('markdown', '资料.markdown'),
|
|
attachment('image', '图片.png'), attachment('large', '过大的资料.pdf', 5 * 1024 * 1024 + 1), attachment('boundary', '可导入.DOCX', 5 * 1024 * 1024)];
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />); await chooseChat();
|
|
for (const name of ['旧文档.doc', '旧表格.xls', '资料.markdown', '图片.png', '过大的资料.pdf']) expect(await screen.findByRole('checkbox', { name: '选择文件 ' + name })).toBeDisabled();
|
|
expect(screen.getAllByText('此格式暂不支持知识库导入')).toHaveLength(4);
|
|
expect(screen.getByText('超过 5 MB,暂不能导入')).toBeVisible();
|
|
expect(screen.getByText('DOCX · 5.0 MB')).toBeVisible();
|
|
await chooseFile('可导入.DOCX');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 1 个文件' }));
|
|
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(imports()).toEqual([expect.objectContaining({ attachment_id: 'boundary' })]);
|
|
});
|
|
|
|
it('distinguishes loading, empty chats, chat read failures and a successful retry', async () => {
|
|
const loading = deferred<{ threads: CloudThread[]; next_offset: null }>();
|
|
const original = api.call.getMockImplementation()!;
|
|
let mode = 'loading';
|
|
api.call.mockImplementation((operation, input) => {
|
|
if (operation !== 'threads') return original(operation, input);
|
|
return mode === 'loading' ? loading.promise : mode === 'error' ? Promise.reject(new Error('目录离线')) : Promise.resolve({ threads, next_offset: null });
|
|
});
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
expect(screen.getByText('正在读取对话…')).toBeVisible();
|
|
expect(screen.queryByText('还没有个人智能体对话。')).not.toBeInTheDocument();
|
|
await act(async () => loading.resolve({ threads: [], next_offset: null }));
|
|
expect(screen.getByText('还没有个人智能体对话。')).toBeVisible();
|
|
view.unmount(); mode = 'error'; render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await screen.findByText('对话读取失败:目录离线');
|
|
expect(screen.queryByText('还没有个人智能体对话。')).not.toBeInTheDocument();
|
|
mode = 'ready'; fireEvent.click(screen.getByRole('button', { name: '重试读取对话' }));
|
|
await screen.findByRole('button', { name: '选择对话 春天的观察讨论' });
|
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('distinguishes file loading, failures, empty files and search results while ignoring a late response from a previous chat', async () => {
|
|
const late = deferred<{ attachments: CloudAttachment[] }>();
|
|
const original = api.call.getMockImplementation()!;
|
|
let readingFails = true;
|
|
api.call.mockImplementation((operation, input) => operation === 'attachments'
|
|
? input.thread_id === 'spring' ? late.promise : readingFails ? Promise.reject(new Error('文件离线')) : Promise.resolve({ attachments: [] })
|
|
: original(operation, input));
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />); await chooseChat();
|
|
expect(screen.getByText('正在读取文件…')).toBeVisible();
|
|
expect(screen.queryByText('这段对话还没有上传文件。')).not.toBeInTheDocument();
|
|
await chooseChat(threads[1].title); await screen.findByText('文件读取失败:文件离线');
|
|
await act(async () => late.resolve({ attachments: sourceFiles.spring }));
|
|
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
|
|
readingFails = false; fireEvent.click(screen.getByRole('button', { name: '重试读取文件' }));
|
|
await screen.findByText('这段对话还没有上传文件。');
|
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
fireEvent.change(screen.getByRole('textbox', { name: '搜索对话' }), { target: { value: '不存在' } });
|
|
expect(screen.getByText('没有匹配的对话。')).toBeVisible();
|
|
});
|
|
|
|
it('guards X, escape and selection during an in-flight import, then closes only after all files finish', async () => {
|
|
const first = deferred<CloudKnowledgeFile>();
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' && input.attachment_id === 'a' ? first.promise : original(operation, input));
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />); await chooseChat(); await chooseFile('观察报告.md'); await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' }));
|
|
expect(imports()).toHaveLength(1);
|
|
expect(screen.getByRole('button', { name: '关闭文件选择' })).toBeDisabled();
|
|
expect(screen.getByRole('checkbox', { name: '选择文件 观察报告.md' })).toBeDisabled();
|
|
expect(screen.queryByRole('button', { name: '放弃重试', exact: true })).not.toBeInTheDocument();
|
|
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
|
|
expect(close).not.toHaveBeenCalled();
|
|
await act(async () => first.resolve(knowledgeFile('a')));
|
|
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(imports()).toHaveLength(2); expect(imported).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('does not read or import without management permission and resumes a confirmed receipt without importing it twice', async () => {
|
|
const receipt = deferred<CloudKnowledgeFile>();
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' ? receipt.promise : original(operation, input));
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} canRead={false} />);
|
|
expect(api.call).not.toHaveBeenCalled(); expect(api.list).not.toHaveBeenCalled();
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} />); await chooseChat(); await chooseFile('观察报告.md');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 1 个文件' }));
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} canRead={false} />);
|
|
await act(async () => receipt.resolve(knowledgeFile('a')));
|
|
expect(imports()).toHaveLength(1); expect(imported).not.toHaveBeenCalled(); expect(close).not.toHaveBeenCalled();
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
expect(pending).toHaveBeenLastCalledWith(true);
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
fireEvent.click(screen.getByRole('button', { name: '继续导入 1 个文件' }));
|
|
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(imports()).toHaveLength(1); expect(imported).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('ignores an old-target response and stops the old batch after a knowledge-base scope change', async () => {
|
|
const response = deferred<CloudKnowledgeFile>();
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' ? response.promise : original(operation, input));
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md'); await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' }));
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} kbId="kb_other" kbName="其他资料" />);
|
|
await act(async () => response.resolve(knowledgeFile('a')));
|
|
expect(imports()).toHaveLength(1); expect(imported).not.toHaveBeenCalled(); expect(close).not.toHaveBeenCalled();
|
|
expect(screen.getByRole('dialog')).toHaveTextContent('导入到「其他资料」');
|
|
expect(screen.queryByRole('list', { name: '文件导入结果' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not continue a batch or notify the old owner after unmount', async () => {
|
|
const response = deferred<CloudKnowledgeFile>();
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' ? response.promise : original(operation, input));
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md'); await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' })); view.unmount();
|
|
await act(async () => response.resolve(knowledgeFile('a')));
|
|
expect(imports()).toHaveLength(1); expect(imported).not.toHaveBeenCalled(); expect(close).not.toHaveBeenCalled();
|
|
expect(pending).toHaveBeenLastCalledWith(false);
|
|
});
|
|
|
|
it('hides the portal after a confirmed loss of ownership so the parent can discard pending requests without importing again', async () => {
|
|
const response = deferred<CloudKnowledgeFile>();
|
|
const original = api.call.getMockImplementation()!;
|
|
let owned = true;
|
|
api.call.mockImplementation((operation, input) => {
|
|
if (operation === 'knowledge') return Promise.resolve({ databases: owned ? [{ kb_id: 'kb_own', name: '我的观察记录' }] : [], models: [] });
|
|
if (operation === 'knowledgeFiles') return Promise.resolve({ files: [], next_offset: null });
|
|
if (operation === 'importKnowledgeAttachment') return response.promise;
|
|
return original(operation, input);
|
|
});
|
|
const created = vi.fn();
|
|
const view = render(<CloudKnowledgePanel slug={slug} onCreated={created} />);
|
|
await screen.findByRole('option', { name: '我的观察记录' });
|
|
fireEvent.change(screen.getByRole('combobox', { name: '管理的知识库' }), { target: { value: 'kb_own' } });
|
|
fireEvent.click(await screen.findByRole('button', { name: '从聊天选择文件' }));
|
|
await chooseChat(); await chooseFile('观察报告.md'); await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' }));
|
|
owned = false;
|
|
view.rerender(<CloudKnowledgePanel slug={slug} onCreated={created} refreshToken={1} />);
|
|
await waitFor(() => expect(screen.queryByRole('dialog', { name: '从聊天选择文件' })).not.toBeInTheDocument());
|
|
await act(async () => response.resolve(knowledgeFile('a')));
|
|
const discard = await screen.findByRole('button', { name: '放弃未确认操作并重新选择' });
|
|
await waitFor(() => expect(discard).toBeEnabled());
|
|
expect(imports()).toHaveLength(1);
|
|
expect(api.call.mock.calls.some(([operation]) => operation === 'processKnowledge')).toBe(false);
|
|
fireEvent.click(discard);
|
|
expect(screen.queryByRole('button', { name: '放弃未确认操作并重新选择' })).not.toBeInTheDocument();
|
|
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toHaveValue('');
|
|
});
|
|
|
|
it('allows cancelling unsubmitted selections and does not mutate knowledge merely by choosing files', async () => {
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />); await chooseChat(); await chooseFile('观察报告.md');
|
|
expect(pending).toHaveBeenLastCalledWith(true);
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭文件选择' }));
|
|
expect(close).toHaveBeenCalledOnce(); expect(imports()).toHaveLength(0); expect(imported).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(['button', 'escape'])('allows %s dismissal during a catalog failure before any import has been sent', async control => {
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md');
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} busy />);
|
|
expect(screen.getByRole('button', { name: '关闭文件选择' })).toBeEnabled();
|
|
if (control === 'button') fireEvent.click(screen.getByRole('button', { name: '关闭文件选择' }));
|
|
else fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
|
|
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(imports()).toHaveLength(0);
|
|
});
|
|
|
|
it('offers a read-only catalog refresh while an uncertain import is blocked, then retries only the original operation', async () => {
|
|
const original = api.call.getMockImplementation()!;
|
|
let fails = true;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' && fails
|
|
? Promise.reject(new Error('结果未确认')) : original(operation, input));
|
|
const refresh = vi.fn();
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} onRefreshKnowledge={refresh} />);
|
|
await chooseChat(); await chooseFile('观察报告.md');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 1 个文件' }));
|
|
await screen.findByText(/导入结果尚未确认:结果未确认/);
|
|
const request = imports()[0];
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} busy onRefreshKnowledge={refresh} />);
|
|
expect(screen.getByRole('button', { name: '关闭文件选择' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: '重试失败的 1 个文件' })).toBeDisabled();
|
|
fireEvent.click(screen.getByRole('button', { name: '刷新知识库', exact: true }));
|
|
expect(refresh).toHaveBeenCalledOnce(); expect(imports()).toHaveLength(1);
|
|
fails = false;
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} onRefreshKnowledge={refresh} />);
|
|
fireEvent.click(screen.getByRole('button', { name: '重试失败的 1 个文件' }));
|
|
await waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(imports()).toEqual([request, request]); expect(imported).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('requires explicit confirmation to abandon a retry during a catalog failure, while keeping the request through cancelling that confirmation', async () => {
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment'
|
|
? Promise.reject(new Error('附件已无法读取')) : original(operation, input));
|
|
const view = render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 1 个文件' }));
|
|
await screen.findByText(/导入结果尚未确认:附件已无法读取/);
|
|
view.rerender(<CloudKnowledgeAttachmentPicker {...props} busy />);
|
|
fireEvent.click(screen.getByRole('button', { name: '放弃重试', exact: true }));
|
|
expect(screen.getByText('这只会结束当前页面的重试。已保存的文件会保留,可刷新知识库确认。')).toBeVisible();
|
|
expect(close).not.toHaveBeenCalled(); expect(pending).toHaveBeenLastCalledWith(true);
|
|
fireEvent.click(screen.getByRole('button', { name: '继续保留' }));
|
|
expect(screen.getByRole('button', { name: '重试失败的 1 个文件' })).toBeDisabled();
|
|
expect(screen.getByRole('list', { name: '文件导入结果' })).toHaveTextContent('观察报告.md');
|
|
fireEvent.click(screen.getByRole('button', { name: '放弃重试', exact: true }));
|
|
fireEvent.click(screen.getByRole('button', { name: '确认放弃重试' }));
|
|
expect(close).toHaveBeenCalledOnce(); expect(pending).toHaveBeenLastCalledWith(false);
|
|
expect(imports()).toHaveLength(1); expect(imported).not.toHaveBeenCalled();
|
|
expect(screen.queryByRole('list', { name: '文件导入结果' })).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('list', { name: '已选聊天文件' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('exits a partly successful batch with a permanent source error without deleting or replaying successful imports', async () => {
|
|
const original = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation((operation, input) => operation === 'importKnowledgeAttachment' && input.attachment_id === 'b'
|
|
? Promise.reject(new Error('原附件已经删除')) : original(operation, input));
|
|
render(<CloudKnowledgeAttachmentPicker {...props} />);
|
|
await chooseChat(); await chooseFile('观察报告.md'); await chooseFile('植物资料.pdf');
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 2 个文件' }));
|
|
await screen.findByText(/导入结果尚未确认:原附件已经删除/);
|
|
expect(imported).toHaveBeenCalledOnce();
|
|
expect(screen.getByRole('button', { name: '关闭文件选择' })).toBeDisabled();
|
|
expect(screen.queryByText(/可继续切换对话选择/)).not.toBeInTheDocument();
|
|
const before = api.call.mock.calls.length;
|
|
fireEvent.click(screen.getByRole('button', { name: '放弃重试', exact: true }));
|
|
fireEvent.click(screen.getByRole('button', { name: '确认放弃重试' }));
|
|
expect(close).toHaveBeenCalledOnce(); expect(pending).toHaveBeenLastCalledWith(false);
|
|
expect(imported).toHaveBeenCalledOnce(); expect(imports()).toHaveLength(2);
|
|
expect(api.call).toHaveBeenCalledTimes(before);
|
|
});
|