Files
makelore/tests/unit/cloud-knowledge-picker.test.tsx
T

383 lines
27 KiB
TypeScript

import { useState } from 'react';
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { ConfigurationFields } from '@/pages/CloudAgents/ConfigurationFields';
import { CloudPendingContext } from '@/pages/CloudAgents/CloudPending';
import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentConfiguration, type CloudCatalog, type CloudKnowledge, 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 slug = 'ml-' + 'a'.repeat(32);
const own = { kb_id: 'own', name: '我的创作资料' };
const models = [{ id: 'embedding', name: '资料向量模型', dimension: 1024 }];
let databases: CloudKnowledge[];
let catalog: CloudCatalog;
const changes = vi.fn();
function Harness({ initial = [], disabled = false, agentSlug = slug }: { initial?: string[]; disabled?: boolean; agentSlug?: string }) {
const [value, setValue] = useState<CloudAgentConfiguration>({ ...EMPTY_CLOUD_CONFIGURATION, model: 'model', knowledges: initial });
return <ConfigurationFields slug={agentSlug} value={value} disabled={disabled} onChange={next => { changes(next); setValue(next); }} />;
}
function choose(id: string) { fireEvent.change(screen.getByRole('combobox', { name: '选择知识库' }), { target: { value: id } }); }
async function openOwn(readyText = '上传第一份文档,让智能体用你的资料回答。') {
await within(screen.getByRole('combobox', { name: '管理的知识库' })).findByRole('option', { name: own.name });
fireEvent.change(screen.getByRole('combobox', { name: '管理的知识库' }), { target: { value: own.kb_id } });
await screen.findByText(readyText);
}
beforeEach(() => {
vi.resetAllMocks(); databases = [own];
catalog = { models: [{ id: 'model', name: '对话模型' }], resources: { knowledges: [
{ key: 'shared', name: '创作指南', description: '平台开放资料' },
{ key: 'other-shared', name: '自然百科' }, { key: own.kb_id, name: own.name },
] }, pricing: { unit: 'points', usage_unit: 'tokens', unit_size: 1000, points_per_unit: '1', version: 1 } };
api.call.mockImplementation(async operation => {
if (operation === 'catalog') return catalog;
if (operation === 'knowledge') return { databases, models };
if (operation === 'knowledgeFiles') return { files: [], next_offset: null };
throw new Error('Unexpected operation ' + operation);
});
});
afterEach(cleanup);
it('joins readable IDs to the owned-only catalog and selects multiple libraries in the draft', async () => {
render(<Harness />);
const sharedGroup = await screen.findByRole('group', { name: 'MakeLore知识' });
expect(within(sharedGroup).getByRole('option', { name: '创作指南' })).toBeInTheDocument();
expect(within(screen.getByRole('group', { name: '我的知识库' })).getByRole('option', { name: own.name })).toBeInTheDocument();
const creation = screen.getByRole('region', { name: '上传自己的文件', exact: true });
const management = screen.getByRole('region', { name: '查看我的知识库', exact: true });
const selection = screen.getByRole('region', { name: '选择知识库', exact: true });
expect(within(creation).getByRole('button', { name: '新建知识库', exact: true })).toBeVisible();
expect(within(creation).queryByRole('combobox', { name: '管理的知识库' })).not.toBeInTheDocument();
expect(within(creation).queryByRole('button', { name: '从电脑上传', exact: true })).not.toBeInTheDocument();
expect(within(management).getByRole('combobox', { name: '管理的知识库' })).toBeVisible();
expect(within(management).getByRole('button', { name: '从电脑上传', exact: true })).toBeDisabled();
expect(within(management).queryByRole('button', { name: '新建知识库', exact: true })).not.toBeInTheDocument();
expect(within(selection).getByRole('combobox', { name: '选择知识库', exact: true })).toBeVisible();
expect(creation.compareDocumentPosition(management) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(management.compareDocumentPosition(selection) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(screen.queryByText('添加到我的知识库', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByText('选择或新建自己的知识库,再从电脑上传资料。选用会加入右侧配置,保存并应用后生效。', { exact: true })).not.toBeInTheDocument();
expect(within(management).queryByText('使用说明与费用', { exact: true })).not.toBeInTheDocument();
expect(within(management).queryByText(/上传到知识库的文档可供/)).not.toBeInTheDocument();
expect(within(selection).queryByText('可多选,每次选择一个知识库加入配置。', { exact: true })).not.toBeInTheDocument();
expect(within(selection).queryByText('选择后保存并应用配置,智能体即可使用这些知识。MakeLore知识只可检索,无法在这里修改。', { exact: true })).not.toBeInTheDocument();
choose('shared'); choose(own.kb_id); choose('other-shared');
expect(within(screen.getByRole('list', { name: '已选知识库' })).getAllByRole('listitem')).toHaveLength(3);
expect(screen.getAllByText('MakeLore知识 · 只读')).toHaveLength(2);
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: ['shared', 'own', 'other-shared'], skills: [] }));
expect(api.call.mock.calls.every(([operation]) => ['catalog', 'knowledge'].includes(operation))).toBe(true);
fireEvent.click(screen.getByRole('button', { name: '移除知识库 创作指南' }));
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: ['own', 'other-shared'] }));
});
it('never offers shared libraries to document management or enables their upload controls', async () => {
databases = [];
render(<Harness initial={['shared']} />);
await screen.findByText('MakeLore知识 · 只读');
const manager = screen.getByRole('combobox', { name: '管理的知识库' });
expect(within(manager).queryByRole('option', { name: '创作指南' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '从电脑上传', exact: true })).toBeDisabled();
expect(screen.queryByRole('button', { name: '删除知识库', exact: true })).not.toBeInTheDocument();
expect(api.call.mock.calls.every(([operation]) => operation !== 'knowledgeFiles')).toBe(true);
});
it('allows readable binding after an owned-list failure while withholding document management and retries both catalogs', async () => {
const original = api.call.getMockImplementation()!;
let failed = true;
api.call.mockImplementation((operation, input) => operation === 'knowledge' && failed
? Promise.reject(new Error('我的目录离线')) : original(operation, input));
render(<Harness />);
await screen.findByText('我的知识库目录暂不可用,仍可选用可读取的知识;上传文件请刷新重试。');
expect(screen.queryByRole('group', { name: 'MakeLore知识' })).not.toBeInTheDocument();
choose('shared');
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeDisabled();
expect(screen.getByRole('button', { name: '从电脑上传', exact: true })).toBeDisabled();
failed = false;
fireEvent.click(screen.getByRole('button', { name: '刷新知识库', exact: true }));
await screen.findByText('MakeLore知识 · 只读');
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeEnabled();
expect(api.call.mock.calls.filter(([operation]) => operation === 'catalog')).toHaveLength(2);
expect(api.call.mock.calls.filter(([operation]) => operation === 'knowledge')).toHaveLength(2);
});
it('retains missing IDs for removal during catalog failure instead of treating them as selectable', async () => {
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'catalog'
? Promise.reject(new Error('读取失败')) : original(operation, input));
render(<Harness initial={['removed-id']} />);
await screen.findByText('知识库目录读取失败,请刷新重试。');
expect(screen.getByText('目录待确认')).toBeVisible();
expect(screen.getByRole('combobox', { name: '选择知识库' })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '移除知识库 removed-id' }));
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: [] }));
});
it('limits bindings to 100 libraries without preventing browsing or creation', async () => {
const selected = Array.from({ length: 100 }, (_, index) => 'old-' + index);
const original = api.call.getMockImplementation()!;
const created = { kb_id: 'new-own', name: '新资料' };
api.call.mockImplementation((operation, input) => {
if (operation === 'createKnowledge') {
databases.push(created);
catalog.resources.knowledges!.push({ key: created.kb_id, name: created.name });
return Promise.resolve(created);
}
return original(operation, input);
});
render(<Harness initial={selected} />);
await screen.findByRole('group', { name: 'MakeLore知识' });
expect(screen.getByRole('combobox', { name: '选择知识库' })).toBeDisabled();
await openOwn();
expect(screen.getByRole('button', { name: '从电脑上传', exact: true })).toBeEnabled();
expect(changes).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
fireEvent.change(screen.getByRole('textbox', { name: '知识库名称' }), { target: { value: created.name } });
fireEvent.change(screen.getByRole('combobox', { name: '向量模型' }), { target: { value: 'embedding' } });
expect(screen.getByRole('button', { name: '创建知识库', exact: true })).toBeEnabled();
fireEvent.click(screen.getByRole('button', { name: '创建知识库', exact: true }));
await waitFor(() => expect(screen.getByRole('combobox', { name: '管理的知识库' })).toHaveValue(created.kb_id));
expect(changes).not.toHaveBeenCalled();
expect(screen.getByRole('combobox', { name: '选择知识库' })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '移除知识库 old-0' }));
await within(screen.getByRole('combobox', { name: '选择知识库' })).findByRole('option', { name: created.name });
choose(created.kb_id);
expect(changes.mock.calls.at(-1)?.[0].knowledges).toHaveLength(100);
expect(changes.mock.calls.at(-1)?.[0].knowledges).toContain(created.kb_id);
});
it('browses an owned library without binding it and only explicit selection changes the draft', async () => {
render(<Harness initial={['shared']} />);
await openOwn();
expect(changes).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '移除知识库 ' + own.name })).not.toBeInTheDocument();
api.uploadKnowledge.mockResolvedValue(null);
fireEvent.click(screen.getByRole('button', { name: '从电脑上传', exact: true }));
await waitFor(() => expect(api.uploadKnowledge).toHaveBeenCalledTimes(1));
await waitFor(() => expect(screen.getByRole('button', { name: '从电脑上传', exact: true })).toBeEnabled());
expect(api.call.mock.calls.every(([operation]) => operation !== 'processKnowledge')).toBe(true);
expect(screen.getByRole('button', { name: '移除知识库 创作指南' })).toBeVisible();
expect(changes).not.toHaveBeenCalled();
choose(own.kb_id);
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: ['shared', own.kb_id] }));
});
it('keeps an upload failure in the management column while the creation form is open', async () => {
api.uploadKnowledge.mockRejectedValue(new Error('上传结果未确认'));
render(<Harness initial={['shared']} />);
await openOwn();
const creation = screen.getByRole('region', { name: '上传自己的文件', exact: true });
const management = screen.getByRole('region', { name: '查看我的知识库', exact: true });
fireEvent.click(within(creation).getByRole('button', { name: '新建知识库', exact: true }));
fireEvent.change(within(creation).getByRole('textbox', { name: '知识库名称' }), { target: { value: '还没创建的资料' } });
fireEvent.click(within(management).getByRole('button', { name: '从电脑上传', exact: true }));
expect(await within(management).findByRole('alert')).toHaveTextContent('上传结果未确认');
expect(within(creation).queryByRole('alert')).not.toBeInTheDocument();
expect(within(creation).getByRole('textbox', { name: '知识库名称' })).toHaveValue('还没创建的资料');
expect(changes).not.toHaveBeenCalled();
});
it('keeps a late creation separate from interleaved draft changes until explicitly selected', async () => {
catalog.models.push({ id: 'model2', name: '另一个模型' });
let resolve!: (value: CloudKnowledge) => void;
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'createKnowledge'
? new Promise(value => { resolve = value; }) : original(operation, input));
render(<Harness initial={['shared']} />);
await screen.findByRole('group', { name: '我的知识库' });
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
fireEvent.change(screen.getByRole('textbox', { name: '知识库名称' }), { target: { value: '新资料' } });
fireEvent.change(screen.getByRole('combobox', { name: '向量模型' }), { target: { value: 'embedding' } });
fireEvent.click(screen.getByRole('button', { name: '创建知识库' }));
choose('other-shared'); fireEvent.change(screen.getByRole('combobox', { name: '模型' }), { target: { value: 'model2' } });
fireEvent.click(screen.getByRole('button', { name: '移除知识库 创作指南' }));
const created = { kb_id: 'new-own', name: '新资料' }; databases.push(created);
catalog.resources.knowledges!.push({ key: created.kb_id, name: created.name });
await act(async () => resolve(created));
await waitFor(() => expect(screen.getByRole('combobox', { name: '管理的知识库' })).toHaveValue(created.kb_id));
expect(screen.queryByRole('button', { name: '移除知识库 新资料' })).not.toBeInTheDocument();
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ model: 'model2', knowledges: ['other-shared'] }));
await within(screen.getByRole('combobox', { name: '选择知识库' })).findByRole('option', { name: created.name });
choose(created.kb_id);
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ model: 'model2', knowledges: ['other-shared', 'new-own'] }));
});
it('removes a deleted owned library from the latest bindings without losing changes made during deletion', async () => {
let resolve!: () => void;
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'deleteKnowledge'
? new Promise<void>(value => { resolve = value; }) : original(operation, input));
render(<Harness initial={['shared', own.kb_id]} />); await openOwn();
fireEvent.click(screen.getByRole('button', { name: '删除知识库', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '确认删除知识内容' }));
choose('other-shared'); fireEvent.click(screen.getByRole('button', { name: '移除知识库 创作指南' }));
databases = []; await act(async () => resolve());
await waitFor(() => expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: ['other-shared'] })));
});
it('ignores an owned-catalog reply from a previous agent after its manager has unmounted', async () => {
let resolve!: (value: unknown) => void;
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'knowledge' && input.slug === slug
? new Promise(value => { resolve = value; }) : original(operation, input));
const view = render(<Harness />);
await screen.findByRole('group', { name: '可选知识库' });
view.rerender(<Harness agentSlug={'ml-' + 'b'.repeat(32)} />);
await screen.findByRole('group', { name: '我的知识库' });
await act(async () => resolve({ databases: [], models: [] }));
expect(within(screen.getByRole('group', { name: '我的知识库' })).getByRole('option', { name: own.name })).toBeInTheDocument();
});
it('keeps an uncertain upload scoped to its library through retry cancellation and releases the guard after acknowledgement', async () => {
databases.push({ kb_id: 'other-own', name: '其他资料' });
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'processKnowledge'
? Promise.resolve({ task_id: 'known-task' }) : original(operation, input));
api.uploadKnowledge.mockRejectedValueOnce(new Error('上传结果未确认')).mockResolvedValueOnce(null)
.mockResolvedValueOnce({ file_id: 'uploaded', name: '资料.txt', size: 10, status: 'uploaded', chunk_count: 0, available: false, error: null });
const pending: Record<string, boolean> = {};
render(<CloudPendingContext.Provider value={(id, value) => { pending[id] = value; }}><Harness initial={['shared']} /></CloudPendingContext.Provider>);
await openOwn();
fireEvent.click(screen.getByRole('button', { name: '从电脑上传', exact: true }));
await screen.findByText('上传结果未确认');
expect(Object.values(pending).some(Boolean)).toBe(true);
const manager = screen.getByRole('combobox', { name: '管理的知识库' });
expect(manager).toBeDisabled(); expect(screen.getByRole('button', { name: '新建知识库' })).toBeDisabled();
fireEvent.change(manager, { target: { value: 'other-own' } }); expect(manager).toHaveValue(own.kb_id);
fireEvent.click(screen.getByRole('button', { name: '重新选择同一文件并重试' }));
await waitFor(() => expect(api.uploadKnowledge).toHaveBeenCalledTimes(2));
await waitFor(() => expect(screen.getByRole('button', { name: '重新选择同一文件并重试' })).toBeEnabled());
expect(manager).toBeDisabled(); expect(Object.values(pending).some(Boolean)).toBe(true);
fireEvent.click(screen.getByRole('button', { name: '重新选择同一文件并重试' }));
await waitFor(() => expect(manager).toBeEnabled());
expect(api.uploadKnowledge.mock.calls.map(call => call[2])).toEqual([expect.any(String), api.uploadKnowledge.mock.calls[0][2], api.uploadKnowledge.mock.calls[0][2]]);
expect(api.call).toHaveBeenCalledWith('processKnowledge', expect.objectContaining({ kb_id: own.kb_id, file_id: 'uploaded' }));
expect(Object.values(pending).some(Boolean)).toBe(false);
});
it('preserves uncertain processing while refreshing and releases it when the server reports a newly acknowledged task', async () => {
const uploaded: CloudKnowledgeFile = { file_id: 'uploaded', name: '资料.txt', size: 10, status: 'uploaded', chunk_count: 0, available: false, error: null };
let files = [uploaded];
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => {
if (operation === 'knowledgeFiles') return Promise.resolve({ files, next_offset: null });
if (operation === 'processKnowledge') return Promise.reject(new Error('处理请求未确认'));
return original(operation, input);
});
render(<Harness />); await openOwn('资料.txt');
fireEvent.click(screen.getByRole('button', { name: '开始处理' }));
await screen.findByText(/文件已保存,处理请求尚未确认/);
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeDisabled();
expect(screen.getByRole('button', { name: '删除', exact: true })).toBeDisabled();
expect(screen.getByRole('button', { name: '删除知识库', exact: true })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
await waitFor(() => expect(screen.getByRole('button', { name: '重试处理' })).toBeEnabled());
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeDisabled();
files = [{ ...uploaded, processing_task: { task_id: 'observed-new-task', status: 'pending', error: null } }];
fireEvent.click(screen.getByRole('button', { name: '刷新状态' }));
await screen.findByText('等待处理');
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeEnabled();
});
it('retains unconfirmed operations in a disabled manager if its owned library disappears and requires explicit discard', async () => {
api.uploadKnowledge.mockRejectedValue(new Error('上传结果未确认'));
render(<Harness initial={['shared']} />); await openOwn();
fireEvent.click(screen.getByRole('button', { name: '从电脑上传', exact: true }));
await screen.findByText('上传结果未确认');
const before = api.call.mock.calls.filter(([operation]) => operation === 'knowledgeFiles').length;
databases = []; fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
await screen.findByText('这个知识库已无法管理。未确认的操作仍保留在当前页面。');
expect(screen.queryByRole('button', { name: '重新选择同一文件并重试' })).not.toBeInTheDocument();
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeDisabled();
expect(api.call.mock.calls.filter(([operation]) => operation === 'knowledgeFiles')).toHaveLength(before);
fireEvent.click(screen.getByRole('button', { name: '放弃未确认操作并重新选择' }));
await waitFor(() => expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeEnabled());
expect(screen.getByRole('button', { name: '移除知识库 创作指南' })).toBeVisible();
});
it('automatically processes a confirmed upload that finishes while both knowledge catalogs are refreshing', async () => {
let finishUpload!: (value: CloudKnowledgeFile) => void;
let finishRefresh!: (value: unknown) => void;
let holdRefresh = false;
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => {
if (operation === 'knowledge' && holdRefresh) return new Promise(value => { finishRefresh = value; });
if (operation === 'processKnowledge') return Promise.resolve({ task_id: 'confirmed-task' });
return original(operation, input);
});
api.uploadKnowledge.mockImplementation(() => new Promise(value => { finishUpload = value; }));
render(<Harness />); await openOwn();
fireEvent.click(screen.getByRole('button', { name: '从电脑上传', exact: true }));
holdRefresh = true; fireEvent.click(screen.getByRole('button', { name: '刷新目录' }));
await screen.findByRole('option', { name: '正在读取我的知识库…' });
await act(async () => finishUpload({ file_id: 'confirmed', name: '资料.txt', size: 10, status: 'uploaded', chunk_count: 0, available: false, error: null }));
expect(api.call).toHaveBeenCalledWith('processKnowledge', expect.objectContaining({ kb_id: own.kb_id, file_id: 'confirmed' }));
await act(async () => finishRefresh({ databases, models }));
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeEnabled();
});
it('finishes an authorized upload through a failed ownership refresh and does not process it again after recovery', async () => {
let finishUpload!: (value: CloudKnowledgeFile) => void;
let rejectRefresh!: (reason: Error) => void;
let holdRefresh = false;
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => {
if (operation === 'knowledge' && holdRefresh) return new Promise((_resolve, reject) => { rejectRefresh = reject; });
if (operation === 'processKnowledge') return Promise.resolve({ task_id: 'confirmed-task' });
return original(operation, input);
});
api.uploadKnowledge.mockImplementation(() => new Promise(value => { finishUpload = value; }));
render(<Harness />); await openOwn();
fireEvent.click(screen.getByRole('button', { name: '从电脑上传', exact: true }));
holdRefresh = true; fireEvent.click(screen.getByRole('button', { name: '刷新知识库', exact: true }));
await screen.findByRole('option', { name: '正在读取我的知识库…' });
await act(async () => rejectRefresh(new Error('目录连接失败')));
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeDisabled();
await act(async () => finishUpload({ file_id: 'confirmed', name: '资料.txt', size: 10, status: 'uploaded', chunk_count: 0, available: false, error: null }));
expect(api.call.mock.calls.filter(([operation]) => operation === 'processKnowledge')).toHaveLength(1);
expect(api.call).toHaveBeenCalledWith('processKnowledge', expect.objectContaining({ kb_id: own.kb_id, file_id: 'confirmed' }));
holdRefresh = false; fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
await waitFor(() => expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeEnabled());
expect(api.uploadKnowledge).toHaveBeenCalledTimes(1);
expect(api.call.mock.calls.filter(([operation]) => operation === 'processKnowledge')).toHaveLength(1);
});
it('retains an authorized creation for management while saving without altering draft bindings', async () => {
let resolve!: (value: CloudKnowledge) => void;
const original = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'createKnowledge'
? new Promise(value => { resolve = value; }) : original(operation, input));
const view = render(<Harness initial={['shared']} />);
await screen.findByRole('group', { name: '我的知识库' });
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
fireEvent.change(screen.getByRole('textbox', { name: '知识库名称' }), { target: { value: '新资料' } });
fireEvent.change(screen.getByRole('combobox', { name: '向量模型' }), { target: { value: 'embedding' } });
fireEvent.click(screen.getByRole('button', { name: '创建知识库' }));
view.rerender(<Harness initial={['shared']} disabled />);
const created = { kb_id: 'new-own', name: '新资料' }; databases.push(created);
catalog.resources.knowledges!.push({ key: created.kb_id, name: created.name });
await act(async () => resolve(created));
await waitFor(() => expect(screen.getByRole('combobox', { name: '管理的知识库' })).toHaveValue(created.kb_id));
expect(changes).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '移除知识库 新资料' })).not.toBeInTheDocument();
expect(screen.getByRole('combobox', { name: '选择知识库' })).toBeDisabled();
});
it('keeps an owned but unusable library manageable without adding an invalid binding and marks a saved unavailable ID', async () => {
const unusable = { kb_id: 'disabled-embedding', name: '旧资料' }; databases.push(unusable);
render(<Harness initial={['shared', unusable.kb_id]} />);
await screen.findByRole('button', { name: '移除知识库 旧资料' });
expect(screen.getByText('已不可用')).toBeVisible();
const manager = screen.getByRole('combobox', { name: '管理的知识库' });
expect(within(manager).getByRole('option', { name: '旧资料(当前暂不可选用)' })).toBeEnabled();
fireEvent.click(screen.getByRole('button', { name: '移除知识库 旧资料' }));
const before = changes.mock.calls.length;
fireEvent.change(manager, { target: { value: unusable.kb_id } });
await screen.findByText('上传第一份文档,让智能体用你的资料回答。');
expect(screen.getByText('这个知识库当前暂不可选用。你仍可管理自己的文件,刷新目录后再试。')).toBeVisible();
expect(changes).toHaveBeenCalledTimes(before);
expect(screen.getByRole('button', { name: '从电脑上传', exact: true })).toBeEnabled();
expect(changes.mock.calls.at(-1)?.[0].knowledges).toEqual(['shared']);
});