| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- import assert from 'node:assert/strict';
- import { execFile } from 'node:child_process';
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
- import { fileURLToPath } from 'node:url';
- import { promisify } from 'node:util';
- import { chromium } from 'playwright';
- const fixture = JSON.parse(await readFile(new URL('../../.work/frontend-business-waiting.json', import.meta.url), 'utf8'));
- const accounts = JSON.parse(await readFile(new URL('../../.work/frontend-accounts.json', import.meta.url), 'utf8'));
- const base = process.env.FRONTEND_URL || 'http://127.0.0.1:18762';
- const started = Date.now();
- 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(15000);
- const content = page.locator('.mobile .content'), errors = [], checks = [];
- let customerToken, supervisorToken, requests = 0, ordinaryInputs = 0, report;
- page.on('pageerror', error => errors.push(error.message));
- page.on('request', request => {
- if (/\/(openapi|admin)\//.test(request.url())) requests++;
- if (request.method() === 'POST' && /\/(inputs|messages)$/.test(new URL(request.url()).pathname)) ordinaryInputs++;
- });
- const timeout = setTimeout(() => { void browser.close(); }, 120000);
- async function request(token, method, path, data, expected = 200) {
- requests++;
- const response = await page.request.fetch(`${base}${path}`, { method, headers: { Authorization: `Bearer ${token || ''}`, 'Idempotency-Key': crypto.randomUUID() }, data });
- assert.equal(response.status(), expected, await response.text());
- return expected === 204 ? null : response.json();
- }
- async function processBusiness() {
- // 只处理持久业务事件,不启动生成工作进程。
- await promisify(execFile)(fileURLToPath(new URL('../../.venv/Scripts/python.exe', import.meta.url)), ['-m', 'zhifu.modules.agent_runtime.business_wait', '--once'], { cwd: fileURLToPath(new URL('../../', import.meta.url)), timeout: 25000, windowsHide: true });
- }
- 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();
- customerToken = (await (await login).json()).access_token;
- await content.getByText('请检查当前方案并确认,确认后提交主管审批。', { exact: false }).waitFor();
- await page.waitForFunction(() => !document.querySelector('.mobile .van-loading'));
- await content.getByLabel('补充问题', { exact: true }).fill('我确认当前方案。');
- assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isDisabled(), true);
- assert.equal(await content.getByRole('button', { name: '停止本次回复', exact: true }).isEnabled(), true);
- assert.equal(await content.getByRole('button', { name: '申请人工协助', exact: true }).isEnabled(), true);
- assert.equal(ordinaryInputs, 0);
- checks.push('业务等待显示本人确认提示,普通聊天不能提交确认,停止与人工协助入口可用');
- await content.getByRole('button', { name: '查看当前方案与进度', exact: true }).click();
- await page.waitForURL('**/#/customer/proposal');
- await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]'));
- const current = await request(customerToken, 'GET', `/openapi/v1/tickets/${fixture.proposal_id}`);
- assert.equal(current.status, 'DRAFT');
- await content.getByText(`方案版本:V${current.proposal_version}`, { exact: true }).waitFor();
- assert.equal(await content.getByRole('button', { name: '确认并提交审批', exact: true }).isDisabled(), false);
- await content.getByRole('checkbox').check();
- const confirming = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/proposals/${fixture.proposal_id}/confirm`));
- await content.getByRole('button', { name: '确认并提交审批', exact: true }).click();
- const confirmResponse = await confirming;
- assert.equal(confirmResponse.status(), 200);
- const confirmed = await confirmResponse.json();
- assert.equal(confirmed.status, 'WAITING_REVIEW');
- assert.equal(confirmed.proposal_id, fixture.proposal_id);
- await page.waitForURL('**/#/customer/ticket');
- await processBusiness();
- await navigate('chat');
- await content.getByText('主管正在审核,处理结果会显示在这里。', { exact: false }).waitFor();
- assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isDisabled(), true);
- assert.equal(await content.getByLabel('补充问题', { exact: true }).inputValue(), '我确认当前方案。');
- const waiting = await request(customerToken, 'GET', `/openapi/v1/agent/runs/${fixture.run_id}`);
- assert.equal(waiting.status, 'WAITING_APPROVAL');
- assert.equal(waiting.waiting.expected_input_type, 'approval_decision');
- assert.equal(waiting.model_calls, 0);
- checks.push('现有方案页执行本人确认,持久业务事件使原运行等待主管审批,未发送保留草稿');
- await navigate('profile');
- await navigate('chat');
- await content.getByText('主管正在审核,处理结果会显示在这里。', { exact: false }).waitFor();
- supervisorToken = (await request(null, 'POST', '/openapi/v1/auth/sessions', { tenant_id: accounts.tenant_id, username: 'supervisor01', password: accounts.password })).access_token;
- const latest = await request(customerToken, 'GET', `/openapi/v1/tickets/${fixture.proposal_id}`);
- await request(supervisorToken, 'POST', `/openapi/v1/approvals/${latest.approval_id}/decisions`, { expected_version: latest.version, proposal_version: latest.proposal_version, decision: 'REJECTED', reason: '业务等待浏览器验收,主管已核查本次申请。' });
- await processBusiness();
- await content.getByText('助手正在查询,请稍候。', { exact: false }).waitFor();
- await content.getByText('审批未通过', { exact: true }).waitFor();
- const resumed = await request(customerToken, 'GET', `/openapi/v1/agent/runs/${fixture.run_id}`);
- assert.equal(resumed.run_id, fixture.run_id);
- assert.equal(resumed.status, 'QUEUED');
- assert.equal(resumed.model_calls, 0);
- assert.equal(ordinaryInputs, 0);
- checks.push('重新进入恢复等待审批,主管决定通过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' });
- assert.deepEqual(errors, []);
- report = { date: new Date().toISOString(), checks, browserErrors: errors, ordinaryInputs, generatedModelCalls: 0, durationLimitSeconds: 120 };
- } finally {
- clearTimeout(timeout);
- if (customerToken) {
- const run = await request(customerToken, 'GET', `/openapi/v1/agent/runs/${fixture.run_id}`);
- if (['WAITING_USER', 'WAITING_APPROVAL', 'QUEUED', 'RUNNING'].includes(run.status)) await request(customerToken, 'POST', `/openapi/v1/agent/runs/${fixture.run_id}/cancel`);
- await request(customerToken, 'DELETE', '/openapi/v1/auth/sessions/current', undefined, 204);
- }
- if (supervisorToken) await request(supervisorToken, 'DELETE', '/openapi/v1/auth/sessions/current', undefined, 204);
- await browser.close();
- if (report) {
- await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
- report = { ...report, seconds: (Date.now() - started) / 1000, requestCount: requests };
- await writeFile(new URL('../../.work/frontend-verification/customer-business-waiting.json', import.meta.url), JSON.stringify(report, null, 2));
- console.log(JSON.stringify(report));
- }
- }
|