import { test, expect, type Page } from '@playwright/test'; import { build, normalizePath } from 'vite'; import { createServer, type Server } from 'node:http'; import { mkdtemp, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve, sep } from 'node:path'; import { createRequire } from 'node:module'; import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft, type CloudKnowledge, type CloudKnowledgeFile } from '../../shared/cloud-agents'; let server: Server; let baseUrl: string; let fixtureDir: string; const fixtureRequire = createRequire(resolve('tests/e2e/cloud-agent-knowledge.spec.ts')); const viteDependencies = createRequire(fixtureRequire.resolve('vite/package.json')); 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 () => { test.setTimeout(90_000); fixtureDir = await realpath(await mkdtemp(join(tmpdir(), 'makelore-knowledge-fixture-'))); await symlink(resolve('node_modules'), join(fixtureDir, 'node_modules'), 'dir'); const html = join(fixtureDir, 'index.html'); const output = join(fixtureDir, 'dist'); await writeFile(html, '
'); const entry = normalizePath(resolve('tests/e2e/__cloud-agent-knowledge-harness.tsx')); // Build the virtual entry once. A static fixture needs no Vite dev server, // background dependency discovery, file watcher, or HMR connection. await build({ configFile: false, root: fixtureDir, publicDir: false, logLevel: 'warn', resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' }, css: { postcss: resolve('.') }, build: { outDir: output, emptyOutDir: true, rollupOptions: { input: html } }, 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 ${JSON.stringify(normalizePath(resolve('src/pages/CloudAgents/index.tsx')))}; import ${JSON.stringify(normalizePath(resolve('src/styles/globals.css')))}; const router = createMemoryRouter([{ path: '/cloud-agents', element: }], { initialEntries: ['/cloud-agents?view=mine'] }); createRoot(document.getElementById('root')).render(
); `; if (id.endsWith('/src/stores/auth.ts')) return 'export function useAuthStore(select) { return select({ user: { userId: "knowledge-fixture" } }); }'; }, }], }); const mime: Record = { html: 'text/html', js: 'text/javascript', css: 'text/css', svg: 'image/svg+xml', png: 'image/png', woff2: 'font/woff2', woff: 'font/woff' }; server = createServer(async (req, res) => { const pathname = decodeURIComponent(new URL(req.url ?? '/', 'http://127.0.0.1').pathname); const file = resolve(output, '.' + (pathname === '/' ? '/index.html' : pathname)); if (!file.startsWith(output + sep)) { res.writeHead(403); res.end(); return; } try { const bytes = await readFile(file); res.setHeader('Content-Type', mime[file.split('.').pop()!] ?? 'application/octet-stream'); res.end(bytes); } catch { res.writeHead(404); res.end(); } }); await new Promise((done, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', done); }); const address = server.address(); if (!address || typeof address === 'string') throw new Error('Fixture HTTP address is unavailable'); baseUrl = `http://127.0.0.1:${address.port}`; }); test.afterAll(async () => { try { if (server?.listening) { server.closeAllConnections(); await new Promise((done, reject) => server.close(error => error ? reject(error) : done())); } } finally { try { if (fixtureDir) await rm(fixtureDir, { recursive: true, force: true }); } finally { (viteDependencies('esbuild') as { stop(): void }).stop(); } } }); 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 | null }; type KnowledgeFixture = { agent: CloudAgentDraft; libraries: CloudKnowledge[]; files: Record; 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 : 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 | 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 'received': return result({ agents: [] }); 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([]); } async function expectKnowledgeLayout(page: Page) { const knowledge = page.getByTestId('cloud-agent-editor').getByRole('group', { name: '知识库', exact: true }); const creation = knowledge.getByRole('region', { name: '上传自己的文件', exact: true }); const management = knowledge.getByRole('region', { name: '查看我的知识库', exact: true }); const selection = knowledge.getByRole('region', { name: '选择知识库', exact: true }); await expect(creation.getByRole('button', { name: '新建知识库', exact: true })).toBeVisible(); await expect(creation.getByRole('combobox', { name: '管理的知识库', exact: true })).toHaveCount(0); await expect(creation.getByRole('button', { name: '从电脑上传', exact: true })).toHaveCount(0); await expect(management.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeVisible(); await expect(management.getByRole('button', { name: '从电脑上传', exact: true })).toBeVisible(); await expect(management.getByRole('button', { name: '新建知识库', exact: true })).toHaveCount(0); await expect(management.locator('summary').filter({ hasText: '使用说明与费用' })).toHaveCount(0); await expect(management.getByText('上传到知识库的文档可供获准使用此智能体的人检索。解析并建立索引后才会生效;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限。知识处理如触发对话模型调用,仍按原模型实际用量计费。', { exact: true })).toHaveCount(0); await expect(selection.getByRole('combobox', { name: '选择知识库', exact: true })).toBeVisible(); await expect(selection.getByText('可多选,每次选择一个知识库加入配置。', { exact: true })).toHaveCount(0); await expect(selection.getByText('选择后保存并应用配置,智能体即可使用这些知识。MakeLore知识只可检索,无法在这里修改。', { exact: true })).toHaveCount(0); await expect(knowledge.getByText('添加到我的知识库', { exact: true })).toHaveCount(0); await expect(knowledge.getByText('选择或新建自己的知识库,再从电脑上传资料。选用会加入右侧配置,保存并应用后生效。', { exact: true })).toHaveCount(0); const creationBounds = await creation.boundingBox(); const managementBounds = await management.boundingBox(); const selectionBounds = await selection.boundingBox(); const ownedBounds = await knowledge.locator('.agent-knowledge-owned-workspace').boundingBox(); expect(creationBounds).not.toBeNull(); expect(managementBounds).not.toBeNull(); expect(selectionBounds).not.toBeNull(); expect(ownedBounds).not.toBeNull(); if (ownedBounds!.width > 840) { expect(creationBounds!.x + creationBounds!.width).toBeLessThanOrEqual(managementBounds!.x + 1); expect(Math.abs(creationBounds!.y - managementBounds!.y)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.height - managementBounds!.height)).toBeLessThanOrEqual(1); expect(selectionBounds!.y).toBeGreaterThanOrEqual(Math.max(creationBounds!.y + creationBounds!.height, managementBounds!.y + managementBounds!.height)); expect(Math.abs(selectionBounds!.x + selectionBounds!.width - managementBounds!.x - managementBounds!.width)).toBeLessThanOrEqual(1); const create = await creation.getByRole('button', { name: '新建知识库', exact: true }).boundingBox(); const manager = await management.getByRole('combobox', { name: '管理的知识库', exact: true }).boundingBox(); const refresh = await management.getByRole('button', { name: '刷新知识库与模型', exact: true }).boundingBox(); const upload = await management.getByRole('button', { name: '从电脑上传', exact: true }).boundingBox(); const controls = await management.locator('.agent-knowledge-management-controls').boundingBox(); for (const control of [manager, refresh, upload]) { expect(Math.abs(control!.y + control!.height / 2 - create!.y - create!.height / 2)).toBeLessThanOrEqual(1); } expect(Math.abs(manager!.width - (controls!.width - 144 - 8) / 2)).toBeLessThanOrEqual(2); if (await creation.getByRole('textbox').count() === 0 && await management.getByRole('combobox', { name: '管理的知识库', exact: true }).inputValue() === '') { expect(Math.abs(create!.y + create!.height / 2 - creationBounds!.y - creationBounds!.height / 2)).toBeLessThanOrEqual(2); } } else { expect(creationBounds!.y + creationBounds!.height).toBeLessThanOrEqual(managementBounds!.y + 1); expect(managementBounds!.y + managementBounds!.height).toBeLessThanOrEqual(selectionBounds!.y + 1); expect(Math.abs(creationBounds!.x - managementBounds!.x)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.width - managementBounds!.width)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.width - selectionBounds!.width)).toBeLessThanOrEqual(1); } expect(Math.abs(creationBounds!.x - selectionBounds!.x)).toBeLessThanOrEqual(1); const choice = await selection.getByRole('combobox', { name: '选择知识库', exact: true }).boundingBox(); const choiceRow = await selection.locator('.agent-knowledge-selection-controls').boundingBox(); if (ownedBounds!.width > 840) { expect(Math.abs(choice!.width - (choiceRow!.width - 102) * 2 / 3)).toBeLessThanOrEqual(2); } const selectedList = selection.getByRole('list', { name: '已选知识库', exact: true }); if (await selectedList.count()) { const selectedBounds = await selectedList.boundingBox(); expect(Math.abs(selectedBounds!.x - choice!.x)).toBeLessThanOrEqual(1); expect(Math.abs(selectedBounds!.width - choice!.width)).toBeLessThanOrEqual(1); } for (const refresh of [management.getByRole('button', { name: '刷新知识库与模型', exact: true }), selection.getByRole('button', { name: '刷新知识库', exact: true })]) { expect(await refresh.evaluate(element => getComputedStyle(element).borderTopWidth)).toBe('1px'); } await expectNoClipping(page); } async function screenshotKnowledge(page: Page, name: string) { const viewport = page.viewportSize()!; const knowledge = page.getByTestId('cloud-agent-editor').getByRole('group', { name: '知识库', exact: true }); const bounds = await knowledge.boundingBox(); // Fit this scrollable card in the viewport so screenshots show every panel. await page.setViewportSize({ width: viewport.width, height: Math.ceil(bounds!.height + 260) }); await knowledge.screenshot({ path: test.info().outputPath(name), animations: 'disabled' }); await page.setViewportSize(viewport); } 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 expect(knowledge.getByRole('heading', { name: '上传自己的文件', exact: true })).toBeVisible(); 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 })).toBeDisabled(); await expectKnowledgeLayout(page); await screenshotKnowledge(page, `knowledge-shared-${width}.png`); 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); if (width === 1440) { for (const intermediateWidth of [768, 1024]) { await page.setViewportSize({ width: intermediateWidth, height: 850 }); await expectKnowledgeLayout(page); await screenshotKnowledge(page, `knowledge-shared-${intermediateWidth}.png`); } } }); test(`own library browsing and creation stay unbound while computer upload processes a document 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 selection = knowledge.getByRole('region', { name: '选择知识库', exact: true }); const choose = selection.getByRole('combobox', { name: '选择知识库', exact: true }); const selected = selection.getByRole('list', { name: '已选知识库', exact: true }); await choose.selectOption('kb_fixturescience'); await expect(knowledge.getByRole('heading', { name: '上传自己的文件', exact: true })).toBeVisible(); expect((await calls(page)).filter(call => call.path.endsWith('/knowledge/pick') || ['createKnowledge', 'processKnowledge'].includes(String(call.input?.operation)))).toEqual([]); const creation = knowledge.getByRole('region', { name: '上传自己的文件', exact: true }); const management = knowledge.getByRole('region', { name: '查看我的知识库', exact: true }); const manager = management.getByRole('combobox', { name: '管理的知识库', exact: true }); const upload = management.getByRole('button', { name: '从电脑上传', exact: true }); const create = creation.getByRole('button', { name: '新建知识库', exact: true }); await expect(upload).toBeDisabled(); await expectKnowledgeLayout(page); await manager.selectOption('kb_fixturepersonal'); await expect(upload).toBeEnabled(); await expect(selected.getByRole('listitem')).toHaveCount(1); await expect(selected).not.toContainText('我的观察记录'); await manager.selectOption(''); await expect(upload).toBeDisabled(); await create.click(); await creation.getByRole('textbox', { name: '知识库名称', exact: true }).fill('我的春天资料'); await creation.getByRole('combobox', { name: '向量模型', exact: true }).selectOption('fixture-embedding'); await expectKnowledgeLayout(page); await creation.getByRole('button', { name: '创建知识库', exact: true }).click(); await expect(manager).toHaveValue('kb_fixturecreated'); await expect(selected.getByRole('listitem')).toHaveCount(1); await expect(selected).not.toContainText('我的春天资料'); await expect(upload).toBeEnabled(); await manager.selectOption('kb_fixturepersonal'); await manager.selectOption('kb_fixturecreated'); await expect(selected.getByRole('listitem')).toHaveCount(1); await expect(knowledge.getByRole('button', { name: '从聊天选择文件', exact: true })).toHaveCount(0); expect((await calls(page)).filter(call => call.path.endsWith('/knowledge/pick') || call.input?.operation === 'processKnowledge')).toEqual([]); await upload.click(); const document = management.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 expect(selected.getByRole('listitem')).toHaveCount(1); await choose.selectOption('kb_fixturecreated'); await expect(selected.getByRole('listitem')).toHaveCount(2); await expect(selected).toContainText('我的春天资料'); await document.scrollIntoViewIfNeeded(); await expectKnowledgeLayout(page); await screenshotKnowledge(page, `knowledge-upload-ready-${width}.png`); // 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 expect(knowledge.getByRole('heading', { name: '上传自己的文件', exact: true })).toBeVisible(); 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 })).not.toContainText('我的观察记录'); await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true }).getByRole('listitem')).toHaveCount(1); // 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(); await page.setViewportSize({ width: 1440, height: 850 }); const managementCard = knowledge.getByRole('region', { name: '查看我的知识库', exact: true }); const managementBounds = await managementCard.boundingBox(); for (const control of await managementCard.locator('.agent-knowledge-management-controls').getByRole('button').all()) { const bounds = await control.boundingBox(); expect(bounds!.x).toBeGreaterThanOrEqual(managementBounds!.x); expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(managementBounds!.x + managementBounds!.width); } await expectNoClipping(page); await page.setViewportSize({ width: 375, height: 850 }); 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('这是我还没有保存的提示词。'); });