import assert from 'node:assert/strict'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { chromium } from 'playwright'; // 准备资料必须来自真实 request_clarification 工具运行;此脚本不替换 HTTP 响应。 const fixture = JSON.parse(await readFile(new URL('../../.work/frontend-waiting.json', import.meta.url), 'utf8')); const started = Date.now(); const base = process.env.FRONTEND_URL || 'http://127.0.0.1:18762'; const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE }); const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); page.setDefaultTimeout(20000); const errors = [], checks = [], resumed = []; let token, report, browserRequests = 0, serviceRequests = 0; page.on('pageerror', error => errors.push(error.message)); page.on('request', request => { if (/\/(openapi|admin)\//.test(request.url())) browserRequests++; if (request.method() === 'POST' && request.url().endsWith('/inputs')) resumed.push(request); }); const content = page.locator('.mobile .content'); const timeout = setTimeout(() => { void browser.close(); }, 90000); async function navigate(target) { await page.evaluate(value => { location.hash = `/customer/${value}`; }, target); await page.locator(`.mobile[data-page="${target}"]`).waitFor(); await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]')); } try { await page.goto(`${base}/#/customer/login`); await content.getByLabel('账户', { exact: true }).fill(fixture.username); await content.getByLabel('密码', { exact: true }).fill(fixture.password); await content.getByRole('checkbox').check(); const login = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/auth/sessions')); await content.getByRole('button', { name: '登录并继续', exact: true }).click(); const loginResponse = await login; assert.equal(loginResponse.status(), 200); token = (await loginResponse.json()).access_token; await content.locator('.message').filter({ hasText: fixture.question }).waitFor(); await content.getByText('请补充助手询问的信息,继续本次咨询。', { exact: false }).waitFor(); await page.waitForFunction(() => [...document.querySelectorAll('.mobile button')].some(button => button.textContent === '发送' && !button.disabled)); assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isEnabled(), true); checks.push('重新登录读取真实等待运行,显示助手问题并开放原输入控件'); await navigate('profile'); await navigate('chat'); await content.getByText('请补充助手询问的信息,继续本次咨询。', { exact: false }).waitFor(); const answer = `键盘空格键无法使用,申请数量为 1。浏览器验证 ${Date.now()}`; await content.getByLabel('补充问题', { exact: true }).fill(answer); const receiving = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/agent/runs/${fixture.run_id}/inputs`)); await content.getByRole('button', { name: '发送', exact: true }).click(); const response = await receiving; assert.equal(response.status(), 202, await response.text()); const result = await response.json(); assert.equal(result.run.run_id, fixture.run_id); assert.equal(result.run.status, 'QUEUED'); assert.equal(result.run.model_calls, 0); assert.equal(resumed.length, 1); assert.equal(resumed[0].postDataJSON().answer, answer); assert.ok(resumed[0].postDataJSON().interrupt_id); assert.ok(resumed[0].postDataJSON().expected_run_version >= 1); assert.ok(resumed[0].headers()['idempotency-key']); await content.locator('.message.customer').filter({ hasText: answer }).waitFor(); assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isDisabled(), true); checks.push('补充资料提交原 run_id、等待标识和版本,真实接口返回 202,SSE 显示本人消息'); const cancelling = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/agent/runs/${fixture.run_id}/cancel`)); await content.getByRole('button', { name: '停止本次回复', exact: true }).click(); assert.equal((await cancelling).status(), 200); await content.getByRole('button', { name: '停止本次回复', exact: true }).waitFor({ state: 'hidden' }); serviceRequests++; const current = await page.request.get(`${base}/openapi/v1/agent/runs/${fixture.run_id}`, { headers: { Authorization: `Bearer ${token}` } }); assert.equal(current.status(), 200); const run = await current.json(); assert.equal(run.status, 'CANCELLED'); assert.equal(run.model_calls, 0); checks.push('恢复后停止原运行,取消事件实时释放输入,生成模型调用为零'); assert.deepEqual(errors, []); report = { date: new Date().toISOString(), checks, browserErrors: errors, inputRequests: resumed.length, generatedModelCalls: run.model_calls }; console.log(checks.join('\n')); } finally { clearTimeout(timeout); if (token) { serviceRequests++; const current = await page.request.get(`${base}/openapi/v1/agent/runs/${fixture.run_id}`, { headers: { Authorization: `Bearer ${token}` } }); if (current.ok() && ['WAITING_USER', 'QUEUED', 'RUNNING'].includes((await current.json()).status)) { serviceRequests++; await page.request.post(`${base}/openapi/v1/agent/runs/${fixture.run_id}/cancel`, { headers: { Authorization: `Bearer ${token}` } }); } serviceRequests++; await page.request.delete(`${base}/openapi/v1/auth/sessions/current`, { headers: { Authorization: `Bearer ${token}` } }); } await browser.close(); if (report) { await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true }); await writeFile(new URL('../../.work/frontend-verification/customer-waiting.json', import.meta.url), JSON.stringify({ ...report, seconds: (Date.now() - started) / 1000, browserRequests, serviceRequests, requestCount: browserRequests + serviceRequests, durationLimitSeconds: 90 }, null, 2)); } }