/** * MetonaAI Desktop — E2E 中断链路(v0.8.2 P3-3) * * 独立应用实例(独立 userData + mock),杜绝跨用例运行态竞态: * 发送挂起脚本(mock 首帧后保持连接、心跳保活)→ 流式态出现中断按钮 → * 点击中断 → 恢复 idle → 再次发送正常消息可完成(恢复力闭环)。 */ import { expect, test } from '@playwright/test'; import { launchApp, type AppHarness } from './app-harness'; import { HANG_PREFIX, MOCK_REPLY } from './mock-llm'; let harness: AppHarness; test.beforeAll(async () => { harness = await launchApp(); }); test.afterAll(async () => { await harness?.cleanup(); }); test('中断:挂起的流式回复 → 中断 → 恢复 idle → 可继续对话', async () => { const page = harness.page; const input = page.locator('[data-chat-input] textarea').first(); await expect(input).toBeVisible({ timeout: 30_000 }); // 发送挂起脚本:mock 推送首帧后保持连接(心跳保活,不会自然结束) await input.click(); await input.fill('请执行 __E2E_HANG__ 脚本'); await input.press('Enter'); // 流式态:挂起脚本的首帧文本可见 await expect(page.getByText(HANG_PREFIX).first()).toBeVisible({ timeout: 30_000 }); // 中断按钮出现(发送按钮在流式态变为「中断」) const stopButton = page.getByRole('button', { name: /中断|Stop/ }).first(); await expect(stopButton).toBeVisible({ timeout: 15_000 }); await stopButton.click(); // 中断后恢复 idle:输入框重新可用 await expect(input).toBeEnabled({ timeout: 15_000 }); // 恢复力闭环:中断后可继续正常对话(新 run → mock 正常回复)。 // abort 点击后引擎需短暂时间真正终止(waitForAbort),期间的发送会被 // 同会话防重入拒绝(busy 提示)—— 以"忙则等待重试"保证确定性。 const busyHint = page.getByText(/该会话正在执行任务|already running/).first(); let sent = false; for (let attempt = 0; attempt < 10 && !sent; attempt++) { await input.click(); await input.fill('中断后请再回复一次'); await input.press('Enter'); try { await busyHint.waitFor({ state: 'visible', timeout: 1_500 }); await page.waitForTimeout(1_000); } catch { sent = true; // 未出现 busy 提示 → 本次发送已被接受 } } expect(sent).toBe(true); // 独立实例中首条 MOCK_REPLY 即恢复对话的回答 await expect(page.getByText(MOCK_REPLY).first()).toBeVisible({ timeout: 30_000 }); });