feat(agents): add grouped knowledge selection and personal uploads
This commit is contained in:
1 parent
a324a3ad0d
commit
3786a18b43
12 files changed
+763
-54
No files matched your search
@@ -0,0 +1,257 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft, type CloudKnowledge, type CloudKnowledgeFile } from '../../shared/cloud-agents';
|
||||
|
||||
let server: ViteDevServer;
|
||||
let baseUrl: string;
|
||||
test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
||||
|
||||
// Render the real editor while replacing only the Main IPC boundary. No Electron,
|
||||
// native picker, real upload or cloud request is started by this suite.
|
||||
test.beforeAll(async () => {
|
||||
const entry = resolve('tests/e2e/__cloud-agent-knowledge-harness.tsx');
|
||||
server = await createServer({
|
||||
configFile: false, cacheDir: 'node_modules/.vite-cloud-agent-knowledge',
|
||||
resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' },
|
||||
server: { host: '127.0.0.1', port: 0 },
|
||||
plugins: [{
|
||||
name: 'isolated-cloud-agent-knowledge', enforce: 'pre',
|
||||
resolveId(id) { if (id === '/__cloud-agent-knowledge-harness.tsx') return entry; },
|
||||
load(id) {
|
||||
if (id === entry) return `
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
||||
import CloudAgents from '/src/pages/CloudAgents/index.tsx';
|
||||
import '/src/styles/globals.css';
|
||||
const router = createMemoryRouter([{ path: '/cloud-agents', element: <CloudAgents /> }], { initialEntries: ['/cloud-agents?view=mine'] });
|
||||
createRoot(document.getElementById('root')).render(<div className="h-screen min-w-0"><RouterProvider router={router} /></div>);
|
||||
`;
|
||||
if (id.endsWith('/src/stores/auth.ts')) return 'export function useAuthStore(select) { return select({ user: { userId: "knowledge-fixture" } }); }';
|
||||
},
|
||||
configureServer(vite) {
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
if (req.url !== '/' && !req.url?.startsWith('/?')) return next();
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<!doctype html><html><head><meta charset="utf-8"></head><body><div id="root"></div><script type="module" src="/__cloud-agent-knowledge-harness.tsx"></script></body></html>');
|
||||
});
|
||||
},
|
||||
}],
|
||||
});
|
||||
await server.listen(); baseUrl = server.resolvedUrls!.local[0];
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
|
||||
const agent: CloudAgentDraft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: '资料小助手', purpose: '用我的资料回答问题。',
|
||||
system_prompt: '请先查资料,再用简单的中文回答。', draft_revision: 1,
|
||||
configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'fixture-model' },
|
||||
published_version: null, enabled: true, updated_at: '2026-10-12T00:00:00Z',
|
||||
};
|
||||
// Existing facade wire format deliberately has no source/can_manage metadata.
|
||||
const libraries: CloudKnowledge[] = [
|
||||
{ kb_id: 'kb_fixturescience', name: '科学知识', description: '平台开放的科学资料', embedding_model: 'fixture-embedding' },
|
||||
{ kb_id: 'kb_fixturereading', name: '阅读知识', description: '平台开放的阅读资料', embedding_model: 'fixture-embedding' },
|
||||
{ kb_id: 'kb_fixturepersonal', name: '我的观察记录', description: '自己的资料', embedding_model: 'fixture-embedding' },
|
||||
];
|
||||
type FixtureCall = { path: string; method: string; input: Record<string, unknown> | null };
|
||||
type KnowledgeFixture = {
|
||||
agent: CloudAgentDraft; libraries: CloudKnowledge[]; files: Record<string, CloudKnowledgeFile[]>;
|
||||
calls: FixtureCall[]; ownedIds: string[]; upload: 'success' | 'cancel' | 'fail'; processFailure: boolean;
|
||||
};
|
||||
type FixtureWindow = Window & { __knowledgeFixture: KnowledgeFixture };
|
||||
|
||||
async function installFixture(page: Page) {
|
||||
page.on('pageerror', error => { throw error; });
|
||||
await page.addInitScript(({ agent, libraries }) => {
|
||||
const scope = window as unknown as FixtureWindow;
|
||||
const stored = sessionStorage.getItem('knowledge-fixture');
|
||||
const state: KnowledgeFixture = stored ? JSON.parse(stored) : {
|
||||
agent, libraries, files: {}, calls: [], ownedIds: ['kb_fixturepersonal'], upload: 'success', processFailure: false,
|
||||
};
|
||||
scope.__knowledgeFixture = state;
|
||||
const persist = () => sessionStorage.setItem('knowledge-fixture', JSON.stringify(state));
|
||||
const result = (json: unknown) => { persist(); return { ok: true, data: { status: 200, ok: true, json: structuredClone(json) } }; };
|
||||
const failure = (message: string) => { persist(); return { ok: true, data: { status: 503, ok: false, json: { error: message } } }; };
|
||||
const managed = (id: unknown) => state.ownedIds.includes(String(id));
|
||||
Object.defineProperty(window, 'electron', { value: { ipcRenderer: {
|
||||
invoke: async (channel: string, request?: { path?: string; method?: string; body?: string }) => {
|
||||
if (channel === 'hostapi:token') return 'fixture';
|
||||
if (channel === 'hostapi:base-url') return 'http://fixture.invalid';
|
||||
if (channel !== 'hostapi:fetch') throw new Error('Unexpected fixture IPC ' + channel);
|
||||
const path = request?.path ?? ''; const method = request?.method ?? 'GET';
|
||||
const input = request?.body ? JSON.parse(request.body) as Record<string, unknown> : null;
|
||||
state.calls.push({ path, method, input });
|
||||
if (path === '/api/cloud-agents/recovery') return result({ recent: null, pending: [] });
|
||||
if (path === '/api/cloud-agents/recovery/recent') return result({ saved: true });
|
||||
if (path.startsWith('/api/cloud-agents/agents')) {
|
||||
if (path === '/api/cloud-agents/agents') return result({ agents: [state.agent], next_cursor: null });
|
||||
if (method === 'PATCH') Object.assign(state.agent, input, { draft_revision: state.agent.draft_revision + 1 });
|
||||
return result(state.agent);
|
||||
}
|
||||
if (path === '/api/cloud-agents/knowledge/pick') {
|
||||
if (!managed(input?.kb_id)) throw new Error('Document upload reached a read-only library');
|
||||
if (state.upload === 'cancel') return result(null);
|
||||
if (state.upload === 'fail') return failure('上传服务暂不可用');
|
||||
const file: CloudKnowledgeFile = { file_id: 'observations-file', name: '春天观察记录.md', size: 1200,
|
||||
status: 'uploaded', error: null, chunk_count: 0, available: false };
|
||||
state.files[String(input?.kb_id)] = [file]; return result(file);
|
||||
}
|
||||
if (path !== '/api/cloud-agents/actions') throw new Error('Unexpected fixture request ' + path);
|
||||
const args = input?.input as Record<string, unknown> | undefined;
|
||||
switch (input?.operation) {
|
||||
case 'catalog': return result({ models: [{ id: 'fixture-model', name: '创作模型' }],
|
||||
resources: { knowledges: state.libraries.map(kb => ({ key: kb.kb_id, name: kb.name, description: kb.description })) }, pricing: null });
|
||||
case 'knowledge': return result({ databases: state.libraries.filter(kb => managed(kb.kb_id)),
|
||||
models: [{ id: 'fixture-embedding', name: '平台向量模型', dimension: 768 }] });
|
||||
case 'access': return result({ published_version: null, enabled: true, share_url: 'niancode://agents/' + agent.slug,
|
||||
api_url: 'http://fixture.invalid', grants: [], applications: [], versions: [] });
|
||||
case 'createKnowledge': {
|
||||
const created: CloudKnowledge = { kb_id: 'kb_fixturecreated', name: String(args?.name), description: '', embedding_model: String(args?.embedding_model) };
|
||||
state.libraries.push(created); state.ownedIds.push(created.kb_id); return result(created);
|
||||
}
|
||||
case 'knowledgeFiles':
|
||||
if (!managed(args?.kb_id)) throw new Error('Document management reached a read-only library');
|
||||
return result({ files: state.files[String(args?.kb_id)] ?? [], next_offset: null });
|
||||
case 'processKnowledge': {
|
||||
if (!managed(args?.kb_id)) throw new Error('Processing reached a read-only library');
|
||||
if (state.processFailure) return failure('处理请求暂不可用');
|
||||
const file = state.files[String(args?.kb_id)]?.find(file => file.file_id === args?.file_id);
|
||||
if (!file) throw new Error('Unknown fixture document');
|
||||
file.processing_task = { task_id: 'processing-task', status: 'pending', error: null };
|
||||
return result({ task_id: 'processing-task' });
|
||||
}
|
||||
default: throw new Error('Unexpected fixture operation ' + String(input?.operation));
|
||||
}
|
||||
},
|
||||
} } });
|
||||
}, { agent, libraries });
|
||||
}
|
||||
|
||||
async function openKnowledge(page: Page) {
|
||||
await page.getByRole('button', { name: '继续制作', exact: true }).click();
|
||||
const editor = page.getByTestId('cloud-agent-editor');
|
||||
await editor.getByRole('tab', { name: '高级设置', exact: true }).click();
|
||||
const knowledge = editor.getByRole('group', { name: '知识库', exact: true });
|
||||
await knowledge.scrollIntoViewIfNeeded();
|
||||
await expect(knowledge.getByRole('combobox', { name: '选择知识库', exact: true })).toBeVisible();
|
||||
await expect(editor.getByRole('combobox', { name: '模型', exact: true })).toBeEnabled();
|
||||
return { editor, knowledge };
|
||||
}
|
||||
async function calls(page: Page) { return page.evaluate(() => (window as unknown as FixtureWindow).__knowledgeFixture.calls); }
|
||||
async function expectNoClipping(page: Page) {
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
const clipped = await page.getByTestId('cloud-agent-editor').evaluate(editor => {
|
||||
const scroll = editor.querySelector('.agent-config-scroll')!; const edge = scroll.getBoundingClientRect();
|
||||
return [...scroll.querySelectorAll('button, input, textarea, select, h2, summary, fieldset')]
|
||||
.filter(element => element.getClientRects().length > 0 && !element.classList.contains('sr-only'))
|
||||
.filter(element => { const bounds = element.getBoundingClientRect(); return bounds.left < edge.left - 1 || bounds.right > edge.right + 1; })
|
||||
.map(element => element.getAttribute('aria-label') ?? element.textContent?.trim());
|
||||
});
|
||||
expect(clipped).toEqual([]);
|
||||
}
|
||||
|
||||
for (const width of [375, 1440]) {
|
||||
test(`multiple shared/personal choices stay draft-only and survive save/reopen at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const { editor, knowledge } = await openKnowledge(page);
|
||||
const choose = knowledge.getByRole('combobox', { name: '选择知识库', exact: true });
|
||||
await expect(choose.locator('optgroup[label="MakeLore知识"] option')).toHaveCount(2);
|
||||
await expect(choose.locator('optgroup[label="我的知识库"] option')).toHaveCount(1);
|
||||
await choose.selectOption('kb_fixturescience'); await choose.selectOption('kb_fixturereading'); await choose.selectOption('kb_fixturepersonal');
|
||||
const selected = knowledge.getByRole('list', { name: '已选知识库', exact: true });
|
||||
await expect(selected.getByRole('listitem')).toHaveCount(3);
|
||||
await selected.getByRole('button', { name: '移除知识库 我的观察记录', exact: true }).click();
|
||||
await expect(selected.getByRole('listitem')).toHaveCount(2);
|
||||
await choose.selectOption('kb_fixturepersonal');
|
||||
await expect(selected.getByRole('listitem')).toHaveCount(3);
|
||||
await expect(selected.getByRole('button', { name: '移除知识库 科学知识', exact: true })).toBeVisible();
|
||||
await expect(selected.getByRole('button', { name: /上传|删除|处理|替换/ })).toHaveCount(0);
|
||||
expect((await calls(page)).filter(call => call.method === 'PATCH' || call.path.endsWith('/knowledge/pick'))).toEqual([]);
|
||||
await knowledge.getByText('上传自己的文件', { exact: true }).click();
|
||||
const management = knowledge.getByRole('combobox', { name: '管理的知识库', exact: true });
|
||||
await expect(management.locator('option[value="kb_fixturescience"], option[value="kb_fixturereading"]')).toHaveCount(0);
|
||||
await expect(knowledge.getByRole('button', { name: '上传文档', exact: true })).toHaveCount(0);
|
||||
await expectNoClipping(page); await page.screenshot({ path: test.info().outputPath(`knowledge-shared-${width}.png`), animations: 'disabled' });
|
||||
await editor.getByRole('button', { name: '保存草稿', exact: true }).click();
|
||||
await expect(editor.getByRole('button', { name: '已保存', exact: true })).toBeVisible();
|
||||
expect((await calls(page)).filter(call => call.method === 'PATCH').map(call => (call.input?.configuration as { knowledges: string[] }).knowledges))
|
||||
.toEqual([['kb_fixturescience', 'kb_fixturereading', 'kb_fixturepersonal']]);
|
||||
await page.reload(); const reopened = await openKnowledge(page);
|
||||
await expect(reopened.knowledge.getByRole('list', { name: '已选知识库', exact: true }).getByRole('listitem')).toHaveCount(3);
|
||||
await expectNoClipping(page);
|
||||
});
|
||||
|
||||
test(`own library creation and explicit upload process a document to ready at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const { editor, knowledge } = await openKnowledge(page);
|
||||
await knowledge.getByRole('combobox', { name: '选择知识库', exact: true }).selectOption('kb_fixturescience');
|
||||
await knowledge.getByText('上传自己的文件', { exact: true }).click();
|
||||
expect((await calls(page)).filter(call => call.path.endsWith('/knowledge/pick') || ['createKnowledge', 'processKnowledge'].includes(String(call.input?.operation)))).toEqual([]);
|
||||
await knowledge.getByRole('button', { name: '新建知识库', exact: true }).click();
|
||||
await knowledge.getByRole('textbox', { name: '知识库名称', exact: true }).fill('我的春天资料');
|
||||
await knowledge.getByRole('combobox', { name: '向量模型', exact: true }).selectOption('fixture-embedding');
|
||||
await knowledge.getByRole('button', { name: '创建并选用', exact: true }).click();
|
||||
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toHaveValue('kb_fixturecreated');
|
||||
await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true })).toContainText('我的春天资料');
|
||||
expect((await calls(page)).filter(call => call.path.endsWith('/knowledge/pick') || call.input?.operation === 'processKnowledge')).toEqual([]);
|
||||
await knowledge.getByRole('button', { name: '上传文档', exact: true }).click();
|
||||
const document = knowledge.getByRole('article', { name: '春天观察记录.md', exact: true });
|
||||
await expect(document.getByText('等待处理', { exact: true })).toBeVisible();
|
||||
const mutations = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick') || call.input?.operation === 'processKnowledge');
|
||||
expect(mutations).toHaveLength(2);
|
||||
expect(mutations[0].input).toMatchObject({ kb_id: 'kb_fixturecreated' });
|
||||
expect(mutations[1].input?.input).toMatchObject({ kb_id: 'kb_fixturecreated', file_id: 'observations-file' });
|
||||
await page.evaluate(() => {
|
||||
const file = (window as unknown as FixtureWindow).__knowledgeFixture.files['kb_fixturecreated'][0];
|
||||
Object.assign(file, { status: 'indexed', chunk_count: 6, available: true,
|
||||
processing_task: { task_id: 'processing-task', status: 'completed', error: null } });
|
||||
});
|
||||
await expect(document.getByText('可用于回答', { exact: true })).toBeVisible({ timeout: 12_000 });
|
||||
await expect(document.getByText('2 KB · 6 段可检索内容', { exact: true })).toBeVisible();
|
||||
await document.scrollIntoViewIfNeeded();
|
||||
await expectNoClipping(page); await page.screenshot({ path: test.info().outputPath(`knowledge-upload-ready-${width}.png`), animations: 'disabled' });
|
||||
// Document mutations never implicitly save the Agent draft.
|
||||
expect((await calls(page)).filter(call => call.method === 'PATCH')).toEqual([]);
|
||||
await editor.getByRole('button', { name: '保存草稿', exact: true }).click();
|
||||
await expect(editor.getByRole('button', { name: '已保存', exact: true })).toBeVisible();
|
||||
expect(await page.evaluate(() => (window as unknown as FixtureWindow).__knowledgeFixture.agent.configuration.knowledges))
|
||||
.toEqual(['kb_fixturescience', 'kb_fixturecreated']);
|
||||
});
|
||||
}
|
||||
|
||||
test('cancelled and failed uploads preserve shared choices and the personal draft', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const { editor, knowledge } = await openKnowledge(page);
|
||||
await editor.getByRole('tab', { name: '介绍与提示词', exact: true }).click();
|
||||
await editor.getByRole('textbox', { name: '提示词', exact: true }).fill('这是我还没有保存的提示词。');
|
||||
await editor.getByRole('tab', { name: '高级设置', exact: true }).click();
|
||||
await knowledge.getByRole('combobox', { name: '选择知识库', exact: true }).selectOption('kb_fixturescience');
|
||||
await knowledge.getByText('上传自己的文件', { exact: true }).click();
|
||||
await knowledge.getByRole('combobox', { name: '管理的知识库', exact: true }).selectOption('kb_fixturepersonal');
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'cancel'; });
|
||||
await knowledge.getByRole('button', { name: '上传文档', exact: true }).click();
|
||||
await expect(knowledge.getByRole('button', { name: '上传文档', exact: true })).toBeEnabled();
|
||||
await expect(knowledge.getByRole('article')).toHaveCount(0);
|
||||
expect((await calls(page)).filter(call => call.input?.operation === 'processKnowledge')).toEqual([]);
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'fail'; });
|
||||
await knowledge.getByRole('button', { name: '上传文档', exact: true }).click();
|
||||
await expect(knowledge.getByRole('alert')).toContainText('上传服务暂不可用');
|
||||
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toHaveValue('kb_fixturepersonal');
|
||||
await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true })).toContainText('科学知识');
|
||||
await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true })).toContainText('我的观察记录');
|
||||
// An uncertain upload response keeps its retry attached to the same library.
|
||||
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeDisabled();
|
||||
await expect(knowledge.getByRole('button', { name: '新建知识库', exact: true })).toBeDisabled();
|
||||
const failedUpload = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick')).at(-1)!;
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'cancel'; });
|
||||
await knowledge.getByRole('button', { name: '重新选择同一文件并重试', exact: true }).click();
|
||||
await expect(knowledge.getByRole('button', { name: '重新选择同一文件并重试', exact: true })).toBeEnabled();
|
||||
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeDisabled();
|
||||
const cancelledRetry = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick')).at(-1)!;
|
||||
expect(cancelledRetry.input?.operation_id).toBe(failedUpload.input?.operation_id);
|
||||
expect((await calls(page)).filter(call => call.method === 'PATCH' || call.input?.operation === 'processKnowledge')).toEqual([]);
|
||||
await expectNoClipping(page);
|
||||
await editor.getByRole('tab', { name: '介绍与提示词', exact: true }).click();
|
||||
await expect(editor.getByRole('textbox', { name: '提示词', exact: true })).toHaveValue('这是我还没有保存的提示词。');
|
||||
});
|
||||
@@ -200,8 +200,8 @@ for (const width of [375, 768, 1024, 1440]) {
|
||||
if (width === 375 || width === 1440) {
|
||||
await page.screenshot({ path: test.info().outputPath(`agent-advanced-upper-${width}.png`), animations: 'disabled' });
|
||||
}
|
||||
await advanced.getByText('管理我的知识文档', { exact: true }).scrollIntoViewIfNeeded();
|
||||
await expect(advanced.getByText('管理我的知识文档', { exact: true })).toBeInViewport();
|
||||
await advanced.getByText('上传自己的文件', { exact: true }).scrollIntoViewIfNeeded();
|
||||
await expect(advanced.getByText('上传自己的文件', { exact: true })).toBeInViewport();
|
||||
await expectConfigurationBounds(page);
|
||||
if (width === 375 || width === 1440) {
|
||||
await page.screenshot({ path: test.info().outputPath(`agent-advanced-lower-${width}.png`), animations: 'disabled' });
|
||||
@@ -238,7 +238,7 @@ for (const width of [375, 1366]) {
|
||||
await ready.getByRole('button', { name: '和它聊聊', exact: true }).click();
|
||||
await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /^(聊天|自动任务|使用与接入)$/ })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '我的智能体', exact: true }).click();
|
||||
await page.getByRole('button', { name: '返回智能体列表', exact: true }).click();
|
||||
await ready.getByRole('button', { name: '调整', exact: true }).click();
|
||||
await expect(page.getByRole('tab', { name: '介绍与提示词', exact: true })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.getByLabel('提示词', { exact: true })).toHaveValue(readyAgent.system_prompt);
|
||||
|
||||
@@ -748,7 +748,7 @@ it('switches editor pages while retaining unfinished knowledge form inputs', asy
|
||||
expect(instructions).not.toBeVisible();
|
||||
expect(screen.getByLabelText('模型')).toBeVisible();
|
||||
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('管理我的知识文档'));
|
||||
fireEvent.click(screen.getByText('上传自己的文件'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
|
||||
const knowledgeName = screen.getByLabelText('知识库名称');
|
||||
fireEvent.change(knowledgeName, { target: { value: '还没创建的资料' } });
|
||||
|
||||
@@ -13,7 +13,7 @@ afterEach(cleanup);
|
||||
|
||||
function openCreation() {
|
||||
render(<CloudKnowledgePanel slug={slug} onCreated={() => undefined} />);
|
||||
fireEvent.click(screen.getByText('管理我的知识文档'));
|
||||
fireEvent.click(screen.getByText('上传自己的文件'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
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 } }); }
|
||||
function openUpload() { fireEvent.click(screen.getByText('上传自己的文件')); }
|
||||
async function openOwn(readyText = '上传第一份文档,让智能体用你的资料回答。') {
|
||||
openUpload();
|
||||
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();
|
||||
expect(screen.getByText('可多选,每次选择一个知识库加入配置。')).toBeVisible();
|
||||
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 mounts their mutating controls', async () => {
|
||||
databases = [];
|
||||
render(<Harness initial={['shared']} />);
|
||||
await screen.findByText('MakeLore知识 · 只读'); openUpload();
|
||||
const manager = screen.getByRole('combobox', { name: '管理的知识库' });
|
||||
expect(within(manager).queryByRole('option', { name: '创作指南' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '上传文档', exact: true })).not.toBeInTheDocument();
|
||||
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'); openUpload();
|
||||
expect(screen.getByRole('combobox', { name: '管理的知识库' })).toBeDisabled();
|
||||
expect(screen.queryByRole('button', { name: '上传文档', exact: true })).not.toBeInTheDocument();
|
||||
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('enforces the 100-library limit and restores selection after removal', async () => {
|
||||
const selected = Array.from({ length: 100 }, (_, index) => 'old-' + index);
|
||||
render(<Harness initial={selected} />);
|
||||
await screen.findByRole('group', { name: 'MakeLore知识' });
|
||||
expect(screen.getByRole('combobox', { name: '选择知识库' })).toBeDisabled();
|
||||
openUpload(); expect(screen.getByRole('button', { name: '新建知识库' })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '移除知识库 old-0' }));
|
||||
choose('shared');
|
||||
expect(changes.mock.calls.at(-1)?.[0].knowledges).toHaveLength(100);
|
||||
expect(changes.mock.calls.at(-1)?.[0].knowledges).toContain('shared');
|
||||
});
|
||||
|
||||
it('selects an existing owned library for uploads and cancelling a file picker does not process a file', async () => {
|
||||
render(<Harness initial={['shared']} />);
|
||||
await openOwn();
|
||||
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: ['shared', own.kb_id] }));
|
||||
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();
|
||||
});
|
||||
|
||||
it('appends a late creation result to the latest bindings after interleaved selection and removal', 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: '我的知识库' }); openUpload();
|
||||
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 screen.findByRole('button', { name: '移除知识库 新资料' });
|
||||
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('adds an already authorized creation result while the editor is temporarily disabled by saving', 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: '我的知识库' }); openUpload();
|
||||
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));
|
||||
expect(changes).toHaveBeenLastCalledWith(expect.objectContaining({ knowledges: ['shared', 'new-own'] }));
|
||||
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(); openUpload();
|
||||
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']);
|
||||
});
|
||||
@@ -22,7 +22,7 @@ beforeEach(() => {
|
||||
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
||||
async function open() {
|
||||
render(<CloudKnowledgePanel slug="ml-agent" onCreated={() => undefined} />);
|
||||
fireEvent.click(screen.getByText('管理我的知识文档'));
|
||||
fireEvent.click(screen.getByText('上传自己的文件'));
|
||||
await screen.findByRole('option', { name: '资料' });
|
||||
fireEvent.change(screen.getByLabelText('管理的知识库'), { target: { value: 'kb' } });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user