customer-business-waiting.mjs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import assert from 'node:assert/strict';
  2. import { execFile } from 'node:child_process';
  3. import { mkdir, readFile, writeFile } from 'node:fs/promises';
  4. import { fileURLToPath } from 'node:url';
  5. import { promisify } from 'node:util';
  6. import { chromium } from 'playwright';
  7. const fixture = JSON.parse(await readFile(new URL('../../.work/frontend-business-waiting.json', import.meta.url), 'utf8'));
  8. const accounts = JSON.parse(await readFile(new URL('../../.work/frontend-accounts.json', import.meta.url), 'utf8'));
  9. const base = process.env.FRONTEND_URL || 'http://127.0.0.1:18762';
  10. const started = Date.now();
  11. const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE });
  12. const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
  13. page.setDefaultTimeout(15000);
  14. const content = page.locator('.mobile .content'), errors = [], checks = [];
  15. let customerToken, supervisorToken, requests = 0, ordinaryInputs = 0, report;
  16. page.on('pageerror', error => errors.push(error.message));
  17. page.on('request', request => {
  18. if (/\/(openapi|admin)\//.test(request.url())) requests++;
  19. if (request.method() === 'POST' && /\/(inputs|messages)$/.test(new URL(request.url()).pathname)) ordinaryInputs++;
  20. });
  21. const timeout = setTimeout(() => { void browser.close(); }, 120000);
  22. async function request(token, method, path, data, expected = 200) {
  23. requests++;
  24. const response = await page.request.fetch(`${base}${path}`, { method, headers: { Authorization: `Bearer ${token || ''}`, 'Idempotency-Key': crypto.randomUUID() }, data });
  25. assert.equal(response.status(), expected, await response.text());
  26. return expected === 204 ? null : response.json();
  27. }
  28. async function processBusiness() {
  29. // 只处理持久业务事件,不启动生成工作进程。
  30. 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 });
  31. }
  32. async function navigate(target) {
  33. await page.evaluate(value => { location.hash = `/customer/${value}`; }, target);
  34. await page.locator(`.mobile[data-page="${target}"]`).waitFor();
  35. await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]'));
  36. }
  37. try {
  38. await page.goto(`${base}/#/customer/login`);
  39. await content.getByLabel('账户', { exact: true }).fill(fixture.username);
  40. await content.getByLabel('密码', { exact: true }).fill(fixture.password);
  41. await content.getByRole('checkbox').check();
  42. const login = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/auth/sessions'));
  43. await content.getByRole('button', { name: '登录并继续', exact: true }).click();
  44. customerToken = (await (await login).json()).access_token;
  45. await content.getByText('请检查当前方案并确认,确认后提交主管审批。', { exact: false }).waitFor();
  46. await page.waitForFunction(() => !document.querySelector('.mobile .van-loading'));
  47. await content.getByLabel('补充问题', { exact: true }).fill('我确认当前方案。');
  48. assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isDisabled(), true);
  49. assert.equal(await content.getByRole('button', { name: '停止本次回复', exact: true }).isEnabled(), true);
  50. assert.equal(await content.getByRole('button', { name: '申请人工协助', exact: true }).isEnabled(), true);
  51. assert.equal(ordinaryInputs, 0);
  52. checks.push('业务等待显示本人确认提示,普通聊天不能提交确认,停止与人工协助入口可用');
  53. await content.getByRole('button', { name: '查看当前方案与进度', exact: true }).click();
  54. await page.waitForURL('**/#/customer/proposal');
  55. await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]'));
  56. const current = await request(customerToken, 'GET', `/openapi/v1/tickets/${fixture.proposal_id}`);
  57. assert.equal(current.status, 'DRAFT');
  58. await content.getByText(`方案版本:V${current.proposal_version}`, { exact: true }).waitFor();
  59. assert.equal(await content.getByRole('button', { name: '确认并提交审批', exact: true }).isDisabled(), false);
  60. await content.getByRole('checkbox').check();
  61. const confirming = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/proposals/${fixture.proposal_id}/confirm`));
  62. await content.getByRole('button', { name: '确认并提交审批', exact: true }).click();
  63. const confirmResponse = await confirming;
  64. assert.equal(confirmResponse.status(), 200);
  65. const confirmed = await confirmResponse.json();
  66. assert.equal(confirmed.status, 'WAITING_REVIEW');
  67. assert.equal(confirmed.proposal_id, fixture.proposal_id);
  68. await page.waitForURL('**/#/customer/ticket');
  69. await processBusiness();
  70. await navigate('chat');
  71. await content.getByText('主管正在审核,处理结果会显示在这里。', { exact: false }).waitFor();
  72. assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isDisabled(), true);
  73. assert.equal(await content.getByLabel('补充问题', { exact: true }).inputValue(), '我确认当前方案。');
  74. const waiting = await request(customerToken, 'GET', `/openapi/v1/agent/runs/${fixture.run_id}`);
  75. assert.equal(waiting.status, 'WAITING_APPROVAL');
  76. assert.equal(waiting.waiting.expected_input_type, 'approval_decision');
  77. assert.equal(waiting.model_calls, 0);
  78. checks.push('现有方案页执行本人确认,持久业务事件使原运行等待主管审批,未发送保留草稿');
  79. await navigate('profile');
  80. await navigate('chat');
  81. await content.getByText('主管正在审核,处理结果会显示在这里。', { exact: false }).waitFor();
  82. supervisorToken = (await request(null, 'POST', '/openapi/v1/auth/sessions', { tenant_id: accounts.tenant_id, username: 'supervisor01', password: accounts.password })).access_token;
  83. const latest = await request(customerToken, 'GET', `/openapi/v1/tickets/${fixture.proposal_id}`);
  84. await request(supervisorToken, 'POST', `/openapi/v1/approvals/${latest.approval_id}/decisions`, { expected_version: latest.version, proposal_version: latest.proposal_version, decision: 'REJECTED', reason: '业务等待浏览器验收,主管已核查本次申请。' });
  85. await processBusiness();
  86. await content.getByText('助手正在查询,请稍候。', { exact: false }).waitFor();
  87. await content.getByText('审批未通过', { exact: true }).waitFor();
  88. const resumed = await request(customerToken, 'GET', `/openapi/v1/agent/runs/${fixture.run_id}`);
  89. assert.equal(resumed.run_id, fixture.run_id);
  90. assert.equal(resumed.status, 'QUEUED');
  91. assert.equal(resumed.model_calls, 0);
  92. assert.equal(ordinaryInputs, 0);
  93. checks.push('重新进入恢复等待审批,主管决定通过SSE更新工单卡片并恢复相同运行');
  94. const cancelling = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/agent/runs/${fixture.run_id}/cancel`));
  95. await content.getByRole('button', { name: '停止本次回复', exact: true }).click();
  96. assert.equal((await cancelling).status(), 200);
  97. await content.getByRole('button', { name: '停止本次回复', exact: true }).waitFor({ state: 'hidden' });
  98. assert.deepEqual(errors, []);
  99. report = { date: new Date().toISOString(), checks, browserErrors: errors, ordinaryInputs, generatedModelCalls: 0, durationLimitSeconds: 120 };
  100. } finally {
  101. clearTimeout(timeout);
  102. if (customerToken) {
  103. const run = await request(customerToken, 'GET', `/openapi/v1/agent/runs/${fixture.run_id}`);
  104. if (['WAITING_USER', 'WAITING_APPROVAL', 'QUEUED', 'RUNNING'].includes(run.status)) await request(customerToken, 'POST', `/openapi/v1/agent/runs/${fixture.run_id}/cancel`);
  105. await request(customerToken, 'DELETE', '/openapi/v1/auth/sessions/current', undefined, 204);
  106. }
  107. if (supervisorToken) await request(supervisorToken, 'DELETE', '/openapi/v1/auth/sessions/current', undefined, 204);
  108. await browser.close();
  109. if (report) {
  110. await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
  111. report = { ...report, seconds: (Date.now() - started) / 1000, requestCount: requests };
  112. await writeFile(new URL('../../.work/frontend-verification/customer-business-waiting.json', import.meta.url), JSON.stringify(report, null, 2));
  113. console.log(JSON.stringify(report));
  114. }
  115. }