feat(trial): assemble completed Agent workspace updates
This commit is contained in:
1 parent
8289f27f3e
commit
7c51bb4a00
38 files changed
+2081
-276
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { createServer, normalizePath, type ViteDevServer } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
import type { CloudAttachment, CloudHistory, CloudInterrupt, CloudRun, CloudThread } from '../../shared/cloud-agents';
|
||||
|
||||
@@ -11,7 +11,7 @@ test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL,
|
||||
// the event stream are isolated fixtures; this suite never starts a native app
|
||||
// or contacts an account, cloud service or model.
|
||||
test.beforeAll(async () => {
|
||||
const entry = resolve('tests/e2e/__cloud-agent-chat-layout-harness.tsx');
|
||||
const entry = normalizePath(resolve('tests/e2e/__cloud-agent-chat-layout-harness.tsx'));
|
||||
server = await createServer({
|
||||
configFile: false,
|
||||
cacheDir: 'node_modules/.vite-cloud-agent-chat-layout',
|
||||
@@ -422,7 +422,7 @@ for (const viewport of [{ width: 375, height: 540 }, { width: 1440, height: 900
|
||||
test(`reasoning streams separately and becomes collapsed durable history at ${viewport.width}×${viewport.height}`, async ({ page }) => {
|
||||
const errors = await openChat(page, 'thinking', viewport.width, viewport.height);
|
||||
const transcript = page.getByTestId('cloud-chat-messages');
|
||||
const thinking = page.getByTestId('cloud-chat-reasoning');
|
||||
const thinking = page.getByTestId('cloud-message-process');
|
||||
const envelope = { thread_id: 'story-first', run_id: 'run-story-first' };
|
||||
const prefix = '先观察故事里的小发现。\n';
|
||||
const reasoning = prefix + Array.from({ length: 40 }, (_, index) =>
|
||||
|
||||
@@ -151,7 +151,7 @@ test('Agent cards open conversation, adjustment and unfinished creation explicit
|
||||
await ready.getByRole('button', { name: '和它聊聊', exact: true }).click();
|
||||
await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId('cloud-agent-editor').getByRole('button', { name: '聊天', exact: true })).toHaveCount(0);
|
||||
await page.getByTestId('cloud-agent-editor').getByRole('button', { name: '我的智能体', exact: true }).click();
|
||||
await page.getByTestId('cloud-agent-editor').getByRole('button', { name: '返回智能体列表', exact: true }).click();
|
||||
await expect(ready).toBeVisible();
|
||||
await ready.getByRole('button', { name: '调整', exact: true }).click();
|
||||
await expect(page.getByRole('tab', { name: '介绍与提示词', exact: true })).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
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';
|
||||
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, type CloudThread, type CloudAttachment } from '../../shared/cloud-agents';
|
||||
|
||||
let server: ViteDevServer;
|
||||
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 () => {
|
||||
const entry = resolve('tests/e2e/__cloud-agent-knowledge-harness.tsx');
|
||||
server = await createServer({
|
||||
configFile: false, cacheDir: 'node_modules/.vite-cloud-agent-knowledge',
|
||||
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, '<!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>');
|
||||
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' },
|
||||
server: { host: '127.0.0.1', port: 0 },
|
||||
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; },
|
||||
@@ -22,25 +38,37 @@ test.beforeAll(async () => {
|
||||
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';
|
||||
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: <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];
|
||||
const mime: Record<string, string> = { 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<void>((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<void>((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(); }
|
||||
}
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
|
||||
const agent: CloudAgentDraft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: '资料小助手', purpose: '用我的资料回答问题。',
|
||||
@@ -54,20 +82,37 @@ const libraries: CloudKnowledge[] = [
|
||||
{ kb_id: 'kb_fixturereading', name: '阅读知识', description: '平台开放的阅读资料', embedding_model: 'fixture-embedding' },
|
||||
{ kb_id: 'kb_fixturepersonal', name: '我的观察记录', description: '自己的资料', embedding_model: 'fixture-embedding' },
|
||||
];
|
||||
const sourceThreads: CloudThread[] = [
|
||||
{ thread_id: 'thread-spring', client_thread_id: 'client-spring', agent_slug: agent.slug, title: '春天的观察讨论', mode: 'preview',
|
||||
updated_at: '2026-10-13T01:00:00Z', run_id: null, status: 'completed', unread: false },
|
||||
{ thread_id: 'thread-reading', client_thread_id: 'client-reading', agent_slug: agent.slug, title: '图书阅读记录', mode: 'preview',
|
||||
updated_at: '2026-10-13T00:00:00Z', run_id: null, status: 'completed', unread: false },
|
||||
];
|
||||
const sourceAttachments: Record<string, CloudAttachment[]> = {
|
||||
'thread-spring': [
|
||||
{ file_id: 'attachment-observations', file_name: '春天观察报告.md', file_size: 1536, path: 'attachments/spring/report.md', original_path: 'attachments/spring/report.md' },
|
||||
{ file_id: 'attachment-plants', file_name: '植物清单.csv', file_size: 2048, path: 'attachments/spring/plants.csv', original_path: 'attachments/spring/plants.csv' },
|
||||
],
|
||||
'thread-reading': [
|
||||
{ file_id: 'attachment-reading', file_name: '阅读笔记.pdf', file_size: 2048, path: 'attachments/reading/notes.pdf', original_path: 'attachments/reading/notes.pdf' },
|
||||
],
|
||||
};
|
||||
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;
|
||||
threads: CloudThread[]; attachments: Record<string, CloudAttachment[]>; importFailures: string[];
|
||||
};
|
||||
type FixtureWindow = Window & { __knowledgeFixture: KnowledgeFixture };
|
||||
|
||||
async function installFixture(page: Page) {
|
||||
page.on('pageerror', error => { throw error; });
|
||||
await page.addInitScript(({ agent, libraries }) => {
|
||||
await page.addInitScript(({ agent, libraries, sourceThreads, sourceAttachments }) => {
|
||||
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,
|
||||
threads: sourceThreads, attachments: sourceAttachments, importFailures: [],
|
||||
};
|
||||
scope.__knowledgeFixture = state;
|
||||
const persist = () => sessionStorage.setItem('knowledge-fixture', JSON.stringify(state));
|
||||
@@ -113,6 +158,19 @@ async function installFixture(page: Page) {
|
||||
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 'threads': return result({ threads: state.threads, next_offset: null });
|
||||
case 'received': return result({ agents: [] });
|
||||
case 'attachments': return result({ attachments: state.attachments[String(args?.thread_id)] ?? [] });
|
||||
case 'importKnowledgeAttachment': {
|
||||
if (!managed(args?.kb_id)) throw new Error('Attachment import reached a read-only library');
|
||||
const attachment = state.attachments[String(args?.thread_id)]?.find(file => file.file_id === args?.attachment_id);
|
||||
if (!attachment) throw new Error('Unknown fixture source attachment');
|
||||
if (state.importFailures.includes(attachment.file_id)) return failure('植物清单导入暂时失败');
|
||||
const file: CloudKnowledgeFile = { file_id: 'imported-' + attachment.file_id, name: attachment.file_name,
|
||||
size: attachment.file_size, status: 'uploaded', error: null, chunk_count: 0, available: false };
|
||||
state.files[String(args?.kb_id)] = [file, ...(state.files[String(args?.kb_id)] ?? []).filter(item => item.file_id !== file.file_id)];
|
||||
return result(file);
|
||||
}
|
||||
case 'processKnowledge': {
|
||||
if (!managed(args?.kb_id)) throw new Error('Processing reached a read-only library');
|
||||
if (state.processFailure) return failure('处理请求暂不可用');
|
||||
@@ -125,7 +183,7 @@ async function installFixture(page: Page) {
|
||||
}
|
||||
},
|
||||
} } });
|
||||
}, { agent, libraries });
|
||||
}, { agent, libraries, sourceThreads, sourceAttachments });
|
||||
}
|
||||
|
||||
async function openKnowledge(page: Page) {
|
||||
@@ -150,6 +208,36 @@ async function expectNoClipping(page: Page) {
|
||||
});
|
||||
expect(clipped).toEqual([]);
|
||||
}
|
||||
async function expectKnowledgeLayout(page: Page, width: number) {
|
||||
const layout = page.locator('.agent-knowledge-workspace');
|
||||
const upload = await layout.locator('.agent-knowledge-upload').boundingBox();
|
||||
const selection = await layout.locator('.agent-knowledge-selection').boundingBox();
|
||||
expect(upload).not.toBeNull(); expect(selection).not.toBeNull();
|
||||
if (width >= 1024) {
|
||||
expect(upload!.x + upload!.width).toBeLessThanOrEqual(selection!.x + 1);
|
||||
expect(Math.abs(upload!.y - selection!.y)).toBeLessThanOrEqual(1);
|
||||
} else {
|
||||
expect(upload!.y + upload!.height).toBeLessThanOrEqual(selection!.y + 1);
|
||||
expect(Math.abs(upload!.x - selection!.x)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
await expectNoClipping(page);
|
||||
}
|
||||
async function expectPickerBounds(page: Page) {
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
const outside = await page.getByRole('dialog', { name: '从聊天选择文件', exact: true }).evaluate(dialog => {
|
||||
const bounds = dialog.getBoundingClientRect();
|
||||
return {
|
||||
bounds: { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom },
|
||||
viewport: { width: innerWidth, height: innerHeight },
|
||||
dialogFits: bounds.left >= 0 && bounds.top >= 0 && bounds.right <= innerWidth && bounds.bottom <= innerHeight,
|
||||
clipped: [...dialog.querySelectorAll('button, input, label, h2, h4')].filter(element => element.getClientRects().length > 0)
|
||||
.filter(element => { const box = element.getBoundingClientRect(); return box.left < bounds.left - 1 || box.right > bounds.right + 1; })
|
||||
.map(element => element.getAttribute('aria-label') ?? element.textContent?.trim()),
|
||||
};
|
||||
});
|
||||
expect(outside.dialogFits, JSON.stringify(outside)).toBe(true); expect(outside.clipped).toEqual([]);
|
||||
if (outside.viewport.width >= 1024) expect(outside.bounds.right - outside.bounds.left).toBeGreaterThanOrEqual(800);
|
||||
}
|
||||
|
||||
for (const width of [375, 1440]) {
|
||||
test(`multiple shared/personal choices stay draft-only and survive save/reopen at ${width}px`, async ({ page }) => {
|
||||
@@ -168,11 +256,11 @@ for (const width of [375, 1440]) {
|
||||
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();
|
||||
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 })).toHaveCount(0);
|
||||
await expectNoClipping(page); await page.screenshot({ path: test.info().outputPath(`knowledge-shared-${width}.png`), animations: 'disabled' });
|
||||
await expect(knowledge.getByRole('button', { name: '从电脑上传', exact: true })).toHaveCount(0);
|
||||
await expectKnowledgeLayout(page, width); 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))
|
||||
@@ -186,7 +274,7 @@ for (const width of [375, 1440]) {
|
||||
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();
|
||||
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([]);
|
||||
await knowledge.getByRole('button', { name: '新建知识库', exact: true }).click();
|
||||
await knowledge.getByRole('textbox', { name: '知识库名称', exact: true }).fill('我的春天资料');
|
||||
@@ -195,7 +283,7 @@ for (const width of [375, 1440]) {
|
||||
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();
|
||||
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');
|
||||
@@ -210,7 +298,7 @@ for (const width of [375, 1440]) {
|
||||
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' });
|
||||
await expectKnowledgeLayout(page, width); 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();
|
||||
@@ -218,6 +306,52 @@ for (const width of [375, 1440]) {
|
||||
expect(await page.evaluate(() => (window as unknown as FixtureWindow).__knowledgeFixture.agent.configuration.knowledges))
|
||||
.toEqual(['kb_fixturescience', 'kb_fixturecreated']);
|
||||
});
|
||||
|
||||
test(`chat files show their source and metadata and import multiple choices into the named own library at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const { knowledge } = await openKnowledge(page);
|
||||
await knowledge.getByRole('combobox', { name: '选择知识库', exact: true }).selectOption('kb_fixturescience');
|
||||
await knowledge.getByRole('combobox', { name: '管理的知识库', exact: true }).selectOption('kb_fixturepersonal');
|
||||
await knowledge.getByRole('button', { name: '从聊天选择文件', exact: true }).click();
|
||||
const picker = page.getByRole('dialog', { name: '从聊天选择文件', exact: true });
|
||||
await expect(picker).toContainText('导入到「我的观察记录」');
|
||||
await picker.getByRole('textbox', { name: '搜索对话', exact: true }).fill('春天');
|
||||
await expect(picker.getByRole('button', { name: '选择对话 图书阅读记录', exact: true })).toHaveCount(0);
|
||||
await picker.getByRole('button', { name: '选择对话 春天的观察讨论', exact: true }).click();
|
||||
await expect(picker.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true })).toBeVisible();
|
||||
await expect(picker.locator('.agent-knowledge-import-file').filter({ has: page.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true }) }))
|
||||
.toContainText(/(?:MD|Markdown)\s*·\s*(?:1\.5|2)\s*KB/i);
|
||||
await picker.getByRole('textbox', { name: '搜索文件', exact: true }).fill('植物');
|
||||
await expect(picker.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true })).toHaveCount(0);
|
||||
await picker.getByRole('checkbox', { name: '选择文件 植物清单.csv', exact: true }).check();
|
||||
await expect(picker.locator('.agent-knowledge-import-file').filter({ has: page.getByRole('checkbox', { name: '选择文件 植物清单.csv', exact: true }) }))
|
||||
.toContainText(/CSV\s*·\s*2\s*KB/);
|
||||
await picker.getByRole('textbox', { name: '搜索文件', exact: true }).fill('');
|
||||
await picker.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true }).check();
|
||||
await picker.getByRole('textbox', { name: '搜索对话', exact: true }).fill('图书');
|
||||
await picker.getByRole('button', { name: '选择对话 图书阅读记录', exact: true }).click();
|
||||
await picker.getByRole('checkbox', { name: '选择文件 阅读笔记.pdf', exact: true }).check();
|
||||
const confirm = picker.getByRole('button', { name: '导入 3 个文件', exact: true });
|
||||
await expect(confirm).toBeEnabled();
|
||||
await expect(confirm).toBeInViewport();
|
||||
expect((await calls(page)).filter(call => ['importKnowledgeAttachment', 'processKnowledge'].includes(String(call.input?.operation)))).toEqual([]);
|
||||
await expectPickerBounds(page);
|
||||
await page.screenshot({ path: test.info().outputPath(`knowledge-chat-picker-${width}.png`), animations: 'disabled' });
|
||||
await confirm.click();
|
||||
await expect(picker).toHaveCount(0);
|
||||
for (const name of ['春天观察报告.md', '植物清单.csv', '阅读笔记.pdf']) {
|
||||
await expect(knowledge.getByRole('article', { name, exact: true })).toBeVisible();
|
||||
}
|
||||
const imports = (await calls(page)).filter(call => call.input?.operation === 'importKnowledgeAttachment');
|
||||
expect(imports).toHaveLength(3);
|
||||
expect(imports.map(call => (call.input?.input as { attachment_id: string }).attachment_id).sort())
|
||||
.toEqual(['attachment-observations', 'attachment-plants', 'attachment-reading']);
|
||||
expect(imports.every(call => (call.input?.input as { kb_id: string }).kb_id === 'kb_fixturepersonal')).toBe(true);
|
||||
expect((await calls(page)).filter(call => call.input?.operation === 'processKnowledge')).toHaveLength(3);
|
||||
expect((await calls(page)).filter(call => call.method === 'PATCH')).toEqual([]);
|
||||
await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true })).toContainText('科学知识');
|
||||
await expectKnowledgeLayout(page, width);
|
||||
});
|
||||
}
|
||||
|
||||
test('cancelled and failed uploads preserve shared choices and the personal draft', async ({ page }) => {
|
||||
@@ -227,15 +361,15 @@ test('cancelled and failed uploads preserve shared choices and the personal draf
|
||||
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 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 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 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('科学知识');
|
||||
@@ -255,3 +389,93 @@ test('cancelled and failed uploads preserve shared choices and the personal draf
|
||||
await editor.getByRole('tab', { name: '介绍与提示词', exact: true }).click();
|
||||
await expect(editor.getByRole('textbox', { name: '提示词', exact: true })).toHaveValue('这是我还没有保存的提示词。');
|
||||
});
|
||||
|
||||
test('a partial chat-file import retries only the failed file with the same operation identity', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const { knowledge } = await openKnowledge(page);
|
||||
await knowledge.getByRole('combobox', { name: '选择知识库', exact: true }).selectOption('kb_fixturescience');
|
||||
await knowledge.getByRole('combobox', { name: '管理的知识库', exact: true }).selectOption('kb_fixturepersonal');
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.importFailures = ['attachment-plants']; });
|
||||
await knowledge.getByRole('button', { name: '从聊天选择文件', exact: true }).click();
|
||||
const picker = page.getByRole('dialog', { name: '从聊天选择文件', exact: true });
|
||||
await picker.getByRole('button', { name: '选择对话 春天的观察讨论', exact: true }).click();
|
||||
await picker.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true }).check();
|
||||
await picker.getByRole('checkbox', { name: '选择文件 植物清单.csv', exact: true }).check();
|
||||
const confirm = picker.getByRole('button', { name: '导入 2 个文件', exact: true });
|
||||
await expect(confirm).toBeInViewport();
|
||||
await confirm.click({ timeout: 15_000 });
|
||||
await expect(picker).toContainText('植物清单导入暂时失败');
|
||||
const retry = picker.getByRole('button', { name: '重试失败的 1 个文件', exact: true });
|
||||
await expect(retry).toBeEnabled();
|
||||
await expect(retry).toBeInViewport();
|
||||
await expect(picker.getByRole('button', { name: '关闭文件选择', exact: true })).toBeDisabled();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(picker).toBeVisible();
|
||||
const originalImports = (await calls(page)).filter(call => call.input?.operation === 'importKnowledgeAttachment');
|
||||
expect(originalImports).toHaveLength(2);
|
||||
await expect(picker.getByRole('list', { name: '文件导入结果', exact: true }).getByRole('listitem').filter({ hasText: '春天观察报告.md' }))
|
||||
.toContainText('已导入,处理状态见知识库');
|
||||
await expectPickerBounds(page);
|
||||
await page.screenshot({ path: test.info().outputPath('knowledge-chat-partial-failure-375.png'), animations: 'disabled' });
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.importFailures = []; });
|
||||
await retry.click(); await expect(picker).toHaveCount(0);
|
||||
await expect(knowledge.getByRole('article', { name: '春天观察报告.md', exact: true })).toBeVisible();
|
||||
await expect(knowledge.getByRole('article', { name: '植物清单.csv', exact: true })).toBeVisible();
|
||||
const imported = (await calls(page)).filter(call => call.input?.operation === 'importKnowledgeAttachment')
|
||||
.map(call => call.input?.input as { attachment_id: string; operation_id: string; kb_id: string });
|
||||
expect(imported.filter(input => input.attachment_id === 'attachment-observations')).toHaveLength(1);
|
||||
const retried = imported.filter(input => input.attachment_id === 'attachment-plants');
|
||||
expect(retried).toHaveLength(2); expect(new Set(retried.map(input => input.operation_id)).size).toBe(1);
|
||||
expect(imported.every(input => input.kb_id === 'kb_fixturepersonal')).toBe(true);
|
||||
const processed = (await calls(page)).filter(call => call.input?.operation === 'processKnowledge')
|
||||
.map(call => (call.input?.input as { file_id: string }).file_id);
|
||||
expect(processed.sort()).toEqual(['imported-attachment-observations', 'imported-attachment-plants']);
|
||||
expect((await calls(page)).filter(call => call.method === 'PATCH')).toEqual([]);
|
||||
await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true })).toContainText('科学知识');
|
||||
await expectNoClipping(page);
|
||||
});
|
||||
|
||||
test('abandoning a failed batch keeps saved files and allows a fresh picker operation without replaying successes', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const { knowledge } = await openKnowledge(page);
|
||||
await knowledge.getByRole('combobox', { name: '选择知识库', exact: true }).selectOption('kb_fixturescience');
|
||||
await knowledge.getByRole('combobox', { name: '管理的知识库', exact: true }).selectOption('kb_fixturepersonal');
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.importFailures = ['attachment-plants']; });
|
||||
await knowledge.getByRole('button', { name: '从聊天选择文件', exact: true }).click();
|
||||
const picker = page.getByRole('dialog', { name: '从聊天选择文件', exact: true });
|
||||
await picker.getByRole('button', { name: '选择对话 春天的观察讨论', exact: true }).click();
|
||||
await picker.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true }).check();
|
||||
await picker.getByRole('checkbox', { name: '选择文件 植物清单.csv', exact: true }).check();
|
||||
await expect(picker.getByRole('button', { name: '导入 2 个文件', exact: true })).toBeInViewport();
|
||||
await picker.getByRole('button', { name: '导入 2 个文件', exact: true }).click({ timeout: 15_000 });
|
||||
await expect(picker.getByRole('button', { name: '重试失败的 1 个文件', exact: true })).toBeEnabled();
|
||||
const importsBefore = (await calls(page)).filter(call => call.input?.operation === 'importKnowledgeAttachment');
|
||||
await picker.getByRole('button', { name: '放弃重试', exact: true }).click();
|
||||
await expect(picker).toContainText('已保存的文件会保留');
|
||||
const abandon = picker.getByRole('button', { name: '确认放弃重试', exact: true });
|
||||
await abandon.scrollIntoViewIfNeeded(); await expect(abandon).toBeInViewport();
|
||||
await expectPickerBounds(page);
|
||||
await page.screenshot({ path: test.info().outputPath('knowledge-chat-abandon-confirm-375.png'), animations: 'disabled' });
|
||||
await abandon.click(); await expect(picker).toHaveCount(0);
|
||||
expect((await calls(page)).filter(call => call.input?.operation === 'importKnowledgeAttachment')).toEqual(importsBefore);
|
||||
expect((await calls(page)).filter(call => /delete/i.test(String(call.input?.operation)) || call.method === 'DELETE')).toEqual([]);
|
||||
await expect(knowledge.getByRole('article', { name: '春天观察报告.md', exact: true })).toBeVisible();
|
||||
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeEnabled();
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.importFailures = []; });
|
||||
await knowledge.getByRole('button', { name: '从聊天选择文件', exact: true }).click();
|
||||
await picker.getByRole('button', { name: '选择对话 春天的观察讨论', exact: true }).click();
|
||||
await expect(picker.getByRole('checkbox', { name: '选择文件 春天观察报告.md', exact: true })).not.toBeChecked();
|
||||
await picker.getByRole('checkbox', { name: '选择文件 植物清单.csv', exact: true }).check();
|
||||
await picker.getByRole('button', { name: '导入 1 个文件', exact: true }).click();
|
||||
await expect(picker).toHaveCount(0);
|
||||
const imports = (await calls(page)).filter(call => call.input?.operation === 'importKnowledgeAttachment')
|
||||
.map(call => call.input?.input as { attachment_id: string; operation_id: string });
|
||||
expect(imports.filter(input => input.attachment_id === 'attachment-observations')).toHaveLength(1);
|
||||
const plants = imports.filter(input => input.attachment_id === 'attachment-plants');
|
||||
expect(plants).toHaveLength(2); expect(new Set(plants.map(input => input.operation_id)).size).toBe(2);
|
||||
const processed = (await calls(page)).filter(call => call.input?.operation === 'processKnowledge')
|
||||
.map(call => (call.input?.input as { file_id: string }).file_id);
|
||||
expect(processed.sort()).toEqual(['imported-attachment-observations', 'imported-attachment-plants']);
|
||||
await expect(knowledge.getByRole('list', { name: '已选知识库', exact: true })).toContainText('科学知识');
|
||||
await expectNoClipping(page);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { createServer, normalizePath, type ViteDevServer } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
let server: ViteDevServer;
|
||||
@@ -9,7 +9,7 @@ test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL,
|
||||
// Exercise actual navigation controls and persisted layout state without
|
||||
// starting Electron or connecting to an account or any cloud service.
|
||||
test.beforeAll(async () => {
|
||||
const entry = resolve('tests/e2e/__cloud-agent-navigation-layout-harness.tsx');
|
||||
const entry = normalizePath(resolve('tests/e2e/__cloud-agent-navigation-layout-harness.tsx'));
|
||||
server = await createServer({
|
||||
configFile: false,
|
||||
cacheDir: 'node_modules/.vite-cloud-agent-navigation-layout',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { createServer, normalizePath, type ViteDevServer } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft } from '../../shared/cloud-agents';
|
||||
|
||||
@@ -10,7 +10,7 @@ test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL,
|
||||
// Render the real workspace and keep all service calls at the Main IPC boundary.
|
||||
// This suite never starts Electron, opens a native file picker or calls a live service.
|
||||
test.beforeAll(async () => {
|
||||
const entry = resolve('tests/e2e/__cloud-agent-platform-avatars-harness.tsx');
|
||||
const entry = normalizePath(resolve('tests/e2e/__cloud-agent-platform-avatars-harness.tsx'));
|
||||
server = await createServer({
|
||||
configFile: false,
|
||||
cacheDir: 'node_modules/.vite-cloud-agent-platform-avatars',
|
||||
|
||||
@@ -18,8 +18,10 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
let draft: Record<string, unknown> | null = null;
|
||||
let recent: unknown = null;
|
||||
let createInput: Record<string, unknown> | null = null;
|
||||
const creationInputs: Record<string, unknown>[] = [];
|
||||
let pending: Record<string, unknown>[] = [];
|
||||
const operations: string[] = [];
|
||||
Reflect.set(globalThis, '__cloudAgentUiFixture', { snapshot: () => ({ draft, createInput, operations }) });
|
||||
Reflect.set(globalThis, '__cloudAgentUiFixture', { snapshot: () => ({ draft, createInput, creationInputs, operations }) });
|
||||
const versions: { version: number; draft_revision: number; created_at: string }[] = [];
|
||||
let knowledgeCatalogLoads = 0;
|
||||
let knowledgeCreated = false;
|
||||
@@ -51,14 +53,15 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
knowledgeUploaded = true;
|
||||
return result({ ...knowledgeFile, status: 'uploaded', chunk_count: 0, available: false, processing_task: null });
|
||||
}
|
||||
if (path.endsWith('/recovery')) return result({ recent, pending: [] });
|
||||
if (path.endsWith('/recovery')) return result({ recent, pending });
|
||||
if (path.endsWith('/recovery/pending') && body.discard === true) { pending = []; return result({ discarded: true }); }
|
||||
if (path.endsWith('/recovery/recent')) { recent = body; return result({ saved: true }); }
|
||||
if (path.endsWith('/actions')) {
|
||||
operations.push(body.operation);
|
||||
const input = body.input ?? {};
|
||||
if (body.operation === 'catalog') return result({
|
||||
models: [{ id: 'test-model', name: '创作模型', description: '模型说明不应显示在配置页' }, { id: 'reasoning-model', name: '思考模型' }],
|
||||
resources: { tools: [{ key: 'fixture-tool', name: '资料整理工具' }], skills: [{ key: 'fixture-skill', name: '文章整理 Skill' }] },
|
||||
resources: { tools: [{ key: 'fixture-tool', name: '资料整理工具' }, { key: 'web_search', name: '上网查资料', description: '在网上查找相关信息。' }], skills: [{ key: 'fixture-skill', name: '文章整理 Skill' }, { key: 'deep-research', name: '查资料,做研究', description: '比较不同说法,再整理出结论和出处。' } ] },
|
||||
pricing: { unit: '词元点数', usage_unit: '词元', unit_size: 1000, points_per_unit: '9.99', version: 1 },
|
||||
});
|
||||
if (body.operation === 'knowledge') return result({ databases: knowledgeCreated ? [{ kb_id: 'kb', name: '我的创作资料' }] : [], models: ++knowledgeCatalogLoads === 1 ? [] : [{ id: 'fixture:test-embedding', name: '资料向量模型', dimension: 1024 }] });
|
||||
@@ -98,7 +101,19 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
if (body.operation === 'files') return result({ files: [] });
|
||||
throw new Error('Unexpected cloud fixture operation ' + body.operation);
|
||||
}
|
||||
if (method === 'POST') { createInput = body; draft = {
|
||||
if (method === 'POST') {
|
||||
creationInputs.push(body);
|
||||
if (creationInputs.length === 1) return { ok: true, data: { status: 422, ok: false,
|
||||
json: { error: '名称没有被接受,请修改后重试', code: 'invalid_name', commandOutcome: 'definitive_failure' } } };
|
||||
if (creationInputs.length === 2) {
|
||||
pending = [{ id: `createAgent::${body.operation_id}`, operation: 'createAgent', input: body, created_at: new Date().toISOString() }];
|
||||
return { ok: true, data: { status: 502, ok: false,
|
||||
json: { error: '暂时无法确认创建结果', code: 'cloud_service_unavailable', commandOutcome: 'unknown' } } };
|
||||
}
|
||||
if (creationInputs.length === 3) return { ok: true, data: { status: 422, ok: false,
|
||||
json: { error: '名称没有被接受,请稍后重试', code: 'invalid_name', commandOutcome: 'definitive_failure' } } };
|
||||
pending = [];
|
||||
createInput = body; draft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: body.name, purpose: body.purpose, system_prompt: body.purpose,
|
||||
icon: body.icon ?? null, icon_url: body.icon === uploadedAvatar.icon ? uploadedAvatar.icon_url : null,
|
||||
draft_revision: 1, updated_at: '2026-09-10T00:00:00Z',
|
||||
@@ -161,11 +176,26 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
await expect(page.getByRole('button', { name: '上一步', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '下一步', exact: true })).toBeDisabled();
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-create-empty-form.png') });
|
||||
await page.getByLabel('名称', { exact: true }).fill('我的写作搭档');
|
||||
await page.getByLabel('名称', { exact: true }).fill('待修改的名字');
|
||||
await page.getByRole('button', { name: '上传头像', exact: true }).click();
|
||||
await expect(page.getByRole('img', { name: '我的写作搭档头像', exact: true })).toHaveAttribute('src', avatar.icon_url);
|
||||
await expect(page.getByRole('img', { name: '待修改的名字头像', exact: true })).toHaveAttribute('src', avatar.icon_url);
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-create-identity.png') });
|
||||
await page.getByRole('button', { name: '下一步', exact: true }).click();
|
||||
await expect(page.getByRole('alert')).toContainText('创建失败');
|
||||
await expect(page.getByLabel('名称', { exact: true })).toBeEnabled();
|
||||
await expect(page.getByRole('button', { name: '更换头像', exact: true })).toBeEnabled();
|
||||
await expect(page.getByRole('complementary', { name: '未确认的云端操作' })).toHaveCount(0);
|
||||
await page.getByLabel('名称', { exact: true }).fill('我的写作搭档');
|
||||
await page.getByRole('button', { name: '下一步', exact: true }).click();
|
||||
await expect(page.getByLabel('名称', { exact: true })).toBeDisabled();
|
||||
await expect(page.getByRole('button', { name: '更换头像', exact: true })).toBeDisabled();
|
||||
await expect(page.getByRole('complementary', { name: '未确认的云端操作' })).toBeVisible();
|
||||
await page.getByRole('button', { name: '移除提醒', exact: true }).click();
|
||||
await expect(page.getByRole('complementary', { name: '未确认的云端操作' })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '重试创建', exact: true }).click();
|
||||
await expect(page.getByRole('alert')).toContainText('创建结果暂未确认');
|
||||
await expect(page.getByLabel('名称', { exact: true })).toBeDisabled();
|
||||
await page.getByRole('button', { name: '重试创建', exact: true }).click();
|
||||
|
||||
const editor = page.getByTestId('cloud-agent-editor');
|
||||
const instructionsTab = page.getByRole('tab', { name: '介绍与提示词', exact: true });
|
||||
@@ -189,6 +219,10 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
const created = await fixtureState();
|
||||
expect(created.createInput).toMatchObject({ name: '我的写作搭档', icon: avatar.icon, purpose: '' });
|
||||
expect(created.createInput).not.toHaveProperty('icon_url');
|
||||
expect(created.creationInputs).toHaveLength(4);
|
||||
expect(created.creationInputs[0].operation_id).not.toBe(created.creationInputs[1].operation_id);
|
||||
expect(created.creationInputs[1]).toEqual(created.creationInputs[2]);
|
||||
expect(created.creationInputs[1]).toEqual(created.creationInputs[3]);
|
||||
expect(await messageOperations()).toEqual([]);
|
||||
|
||||
await page.getByRole('button', { name: '修改头像', exact: true }).click();
|
||||
@@ -214,7 +248,11 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
await expect(page.getByRole('heading', { name: '费用上限', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('heading', { name: '运行限制', exact: true })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '选择工具箱', exact: true }).click();
|
||||
await expect(page.getByRole('checkbox', { name: /上网查资料.*在网上查找相关信息/ })).toBeVisible();
|
||||
await page.getByRole('checkbox', { name: '资料整理工具', exact: true }).check();
|
||||
await page.getByRole('button', { name: '选择做事手册', exact: true }).click();
|
||||
await page.getByLabel('搜索做事手册', { exact: true }).fill('出处');
|
||||
await expect(page.getByRole('checkbox', { name: /查资料,做研究.*出处/ })).toBeVisible();
|
||||
await instructionsTab.click();
|
||||
await expect(introduction).toHaveValue('一起梳理故事和文章');
|
||||
await expect(prompt).toHaveValue(initialPrompt);
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test('personal chat displays thinking, tools and Markdown, then restores them after reload', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
const applicationUrl = page.url();
|
||||
await page.goto('about:blank');
|
||||
// Cloud provider is deterministic here; the real history HTTP/PG boundary is tested in Yuxi.
|
||||
await app.evaluate(({ ipcMain, BrowserWindow }) => {
|
||||
BrowserWindow.getAllWindows()[0].setSize(1180, 820);
|
||||
let complete = false;
|
||||
Object.assign(globalThis, { finishRenderRun: () => { complete = true; } });
|
||||
const slug = 'ml-' + 'a'.repeat(32);
|
||||
const configuration = { model: 'test', tools: [], knowledges: [], mcps: [], skills: [], preload_skills: [], subagents: [], tool_approval_mode: 'always_trust', max_execution_steps: 300, max_output_tokens: 4096, max_run_seconds: 600 };
|
||||
const draft = { slug, name: '渲染测试助手', purpose: '整理资料', system_prompt: '', configuration,
|
||||
draft_revision: 1, published_version: 1, enabled: true, updated_at: '' };
|
||||
const run = { agent_run_id: 'render-run', request_id: 'render-request', thread_id: 'render-thread', agent_slug: slug, status: 'running', output: '', version: '1' };
|
||||
const thread = { thread_id: 'render-thread', client_thread_id: 'client', agent_slug: slug, title: '整理资料', mode: 'published', updated_at: '', run_id: run.agent_run_id, unread: false, status: 'running' };
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; body?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
const body = request.body ? JSON.parse(request.body) : {};
|
||||
if (path === '/api/auth/session/sync') return result({ success: true, session: { accessToken: 'fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false } });
|
||||
if (path === '/api/auth/me') return result({ success: true, user: { username: 'creator', userId: 'creator', tenantId: null, deptId: null, authorities: [] }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true } });
|
||||
if (path === '/api/works/user/agent-profile') return result({ success: true, profile: { display_name: '创作者', completed: true, version: 1 } });
|
||||
if (path.endsWith('/recovery')) return result({ recent: null, pending: [] });
|
||||
if (path.endsWith('/recovery/recent')) return result({});
|
||||
if (path.endsWith('/agents') || path.endsWith('/bootstrap')) return result({ agents: [draft], next_cursor: null });
|
||||
if (path.endsWith('/actions')) {
|
||||
if (body.operation === 'threads') return result({ threads: [thread], next_offset: null });
|
||||
if (body.operation === 'history') return result({ thread_id: thread.thread_id, run: { ...run, status: complete ? 'completed' : 'running' }, next_offset: null, messages: complete ? [{
|
||||
id: 1, message_id: 'answer', role: 'assistant', run_id: run.agent_run_id, request_id: run.request_id, created_at: '',
|
||||
content: '## 整理结果\n\n| 项目 | 状态 |\n| --- | --- |\n| 资料 | 已核对 |\n\n**可以使用**',
|
||||
reasoning_content: '先核对来源,再整理结果。',
|
||||
tool_calls: [{ id: 'lookup', name: 'web_search', display_name: '上网查资料', display_description: '在网上查找相关信息。', args: { query: '实际检索资料' }, status: 'success', tool_call_result: { content: '找到两条资料' } }],
|
||||
}] : [] });
|
||||
if (body.operation === 'run') return result({ ...run, status: complete ? 'completed' : 'running' });
|
||||
if (body.operation === 'viewed') return result({});
|
||||
if (body.operation === 'files') return result({ files: [] });
|
||||
if (body.operation === 'attachments') return result({ attachments: [] });
|
||||
if (body.operation === 'catalog') return result({ models: [{ id: 'test', name: '测试模型' }], resources: {}, pricing: null });
|
||||
if (body.operation === 'access') return result({ published_version: 1, enabled: true, versions: [], grants: [], applications: [], share_url: '', api_url: '' });
|
||||
if (body.operation === 'channelBindings') return result({ items: [], available_accounts: [] });
|
||||
return result({});
|
||||
}
|
||||
if (path.startsWith('/api/cloud-agents/agents/')) return result(draft);
|
||||
return result({ success: true });
|
||||
});
|
||||
});
|
||||
await page.addInitScript(() => {
|
||||
const sources: EventTarget[] = [];
|
||||
Object.assign(window, { renderSources: sources });
|
||||
class FixtureSource extends EventTarget {
|
||||
onopen = null; onerror = null; close() {}
|
||||
constructor(url: string) { super(); if (url.includes('/cloud-agents/runs/')) sources.push(this); }
|
||||
}
|
||||
Object.defineProperty(window, 'EventSource', { value: FixtureSource });
|
||||
});
|
||||
await page.goto(applicationUrl);
|
||||
await page.getByTestId('ai-module-option-cloud_agents').click();
|
||||
await page.getByRole('article', { name: '渲染测试助手', exact: true }).getByRole('button', { name: '和它聊聊', exact: true }).click();
|
||||
const emit = async (items: unknown[], cursor: string) => page.evaluate(({ items, cursor }) => {
|
||||
const sources = (window as unknown as { renderSources: EventTarget[] }).renderSources;
|
||||
sources.at(-1)!.dispatchEvent(new MessageEvent('messages', { lastEventId: cursor, data: JSON.stringify({ run_id: 'render-run', thread_id: 'render-thread', payload: { items } }) }));
|
||||
}, { items, cursor });
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as { renderSources: EventTarget[] }).renderSources.length)).toBeGreaterThan(0);
|
||||
await emit([{ stream_event: { type: 'message_delta', message_id: 'answer', reasoning_content: '先核对来源,再整理结果。' } }], '100-1');
|
||||
const process = page.getByTestId('cloud-message-process');
|
||||
await expect(page.getByLabel('智能体思考过程')).toBeVisible();
|
||||
await emit([
|
||||
{ stream_event: { type: 'tool_call', message_id: 'answer', tool_call_id: 'lookup', name: 'web_search', display_name: '上网查资料', display_description: '在网上查找相关信息。', args: { query: '测试资料' } } },
|
||||
{ event: { method: 'tools', data: { event: 'tool-started', tool_call_id: 'lookup', input: { query: '实际检索资料' } } } },
|
||||
{ event: { method: 'tools', data: { event: 'tool-finished', tool_call_id: 'lookup', output: { content: '找到两条资料' } } } },
|
||||
{ stream_event: { type: 'message_delta', message_id: 'answer', content: '## 整理结果\n\n| 项目 | 状态 |\n| --- | --- |\n| 资料 | 已核对 |\n\n**可以使用**' } },
|
||||
], '100-2');
|
||||
await expect(page.getByRole('heading', { name: '整理结果' })).toBeVisible();
|
||||
await expect(page.getByRole('table')).toBeVisible();
|
||||
await process.locator(':scope > summary').click();
|
||||
await process.getByText('上网查资料 · 已完成', { exact: true }).click();
|
||||
await expect(process.getByText('找到两条资料', { exact: true })).toBeVisible();
|
||||
await expect(process.getByText('在网上查找相关信息。', { exact: true })).toBeVisible();
|
||||
await expect(process.getByText('工具标识:web_search', { exact: true })).toBeVisible();
|
||||
await expect(process.getByText('"实际检索资料"', { exact: false })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('personal-chat-process.png') });
|
||||
await app.evaluate(() => (globalThis as unknown as { finishRenderRun: () => void }).finishRenderRun());
|
||||
await page.evaluate(() => (window as unknown as { renderSources: EventTarget[] }).renderSources.at(-1)!.dispatchEvent(new MessageEvent('end', { data: JSON.stringify({ run_id: 'render-run', thread_id: 'render-thread', payload: { status: 'completed' } }) })));
|
||||
await expect(page.getByRole('heading', { name: '整理结果' })).toHaveCount(1);
|
||||
await page.reload();
|
||||
await page.getByRole('article', { name: '渲染测试助手', exact: true }).getByRole('button', { name: '和它聊聊', exact: true }).click();
|
||||
await expect(page.getByRole('heading', { name: '整理结果' })).toBeVisible();
|
||||
await page.getByTestId('cloud-message-process').locator(':scope > summary').click();
|
||||
await expect(page.getByLabel('智能体思考过程')).toHaveText('先核对来源,再整理结果。');
|
||||
await page.getByText('上网查资料 · 已完成', { exact: true }).click();
|
||||
await expect(page.getByText('在网上查找相关信息。', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('工具标识:web_search', { exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('personal-chat-reopened.png') });
|
||||
} finally { await closeElectronApp(app); }
|
||||
});
|
||||
Reference in new issue
Block a user