Files
metona-ai-desktop/e2e/metona.spec.ts
T
thzxx 4cd6e997b5
CI / 类型检查 + Lint + 单元测试 (push) Failing after 9m45s
CI / 全量测试 (Electron ABI) (push) Failing after 6m28s
CI / 产物编译验证 (push) Successful in 11m18s
feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
2026-09-08 14:30:27 +08:00

82 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MetonaAI Desktop — E2E 冒烟测试(v0.8.1 P2-5 引入,v0.8.2 P3-3 扩充)
*
* 端到端链路(Playwright + Electron):
* 启动应用(隔离 userData)→ 跳过引导(种子配置)→ 新建会话 → 输入消息 →
* 发送 → 引擎调用本地 mock LLM(SSE 流式)→ 断言回复渲染 + mock 收到合法
* 请求体(携带「最大输出上限」设置值)+ 多轮工具调用闭环。
*
* 中断链路见 interrupt.spec.ts(独立应用实例,杜绝跨用例运行态竞态)。
*
* 契约:不触外网(LLM 指向 127.0.0.1 随机端口 mock)、不污染真实用户数据
* METONA_USER_DATA_DIR 重定向)、断言的配置值即设置面板合法配置项。
*/
import { expect, test } from '@playwright/test';
import { launchApp, type AppHarness } from './app-harness';
import { MOCK_REPLY, TOOL_REPLY } from './mock-llm';
let harness: AppHarness;
test.beforeAll(async () => {
harness = await launchApp();
});
test.afterAll(async () => {
await harness?.cleanup();
});
test('冒烟:发送消息 → mock LLM 流式回复渲染到会话', async () => {
// 等待聊天输入框可用(配置加载 + 工具就绪后启用)
// InputBase 把 data-chat-input 挂在根 div —— 实际可编辑元素是内部 textarea
//MUI 另渲染一个 readonly 隐藏 textarea 用于测量,需用 .first() 取可交互的)
const input = harness.page.locator('[data-chat-input] textarea').first();
await expect(input).toBeVisible({ timeout: 30_000 });
// 发送一条用户消息
await input.click();
await input.fill('你好,请做一个冒烟回复');
await input.press('Enter');
// mock LLM 的流式回复出现在聊天区(引擎 → SSE → 渲染层全链路)
await expect(harness.page.getByText(MOCK_REPLY).first()).toBeVisible({ timeout: 30_000 });
// 用户消息持久化回显
await expect(harness.page.getByText('你好,请做一个冒烟回复').first()).toBeVisible();
});
test('冒烟:引擎请求体携带设置面板「最大输出上限」配置(2048)', async () => {
// beforeAll 后发送的第一条消息已在 mock.requests 中(顺序执行时上一测试已完成)
const chatRequests = harness.mock.requests.filter((r) => 'messages' in r);
expect(chatRequests.length).toBeGreaterThanOrEqual(1);
// llm.maxTokens 原样透传(无任何按模型钳制)
expect(chatRequests[0].max_tokens).toBe(2048);
// 请求发往本地 mock(经 adapter 组装),模型名来自种子配置
expect(chatRequests[0].model).toBe('deepseek-v4-flash');
});
test('冒烟:多轮工具调用 → 工具结果回传 → 最终回答渲染', async () => {
const input = harness.page.locator('[data-chat-input] textarea').first();
await expect(input).toBeVisible({ timeout: 30_000 });
await input.click();
await input.fill('请执行 __E2E_TOOL_CALL__ 脚本');
await input.press('Enter');
// 第二轮:mock 收到带 tool 消息的请求后返回最终回答(ReAct 闭环全链路)
await expect(harness.page.getByText(TOOL_REPLY).first()).toBeVisible({ timeout: 30_000 });
// 工具调用卡片(think)存在于聊天流(卡片容器可能滚动出视口,用 attached 断言)
await expect(harness.page.getByText('think').first()).toBeAttached();
// mock 至少收到两条 chat 请求:首轮 tool_calls + 次轮带 tool 结果
const chatRequests = harness.mock.requests.filter(
(r) => 'messages' in r && (r as { messages?: unknown[] }).messages?.length,
);
const toolCallReq = chatRequests.find((r) => JSON.stringify(r).includes('__E2E_TOOL_CALL__'));
const toolResultReq = chatRequests.find((r) =>
(r as { messages?: Array<{ role?: string }> }).messages?.some((m) => m.role === 'tool'),
);
expect(toolCallReq).toBeDefined();
expect(toolResultReq).toBeDefined();
});