test_answer_cache_ui.cjs 5.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. const assert = require('node:assert/strict');
  2. const fs = require('node:fs');
  3. const path = require('node:path');
  4. const {chromium} = require('playwright');
  5. const root = path.resolve(__dirname, '../html');
  6. (async () => {
  7. const browser = await chromium.launch({headless:true,channel:'msedge'});
  8. try {
  9. const page = await browser.newPage({viewport:{width:1280,height:900}});
  10. const requests = [];
  11. let failNext = false;
  12. await page.route('**/*', async route => {
  13. const url = new URL(route.request().url());
  14. if (url.pathname.endsWith('/answer-cache')) {
  15. requests.push({method:route.request().method(), path:url.pathname, body:route.request().postDataJSON()});
  16. if (failNext) { failNext=false; return route.fulfill({status:409,json:{detail:'数据已更新,请重新提问后确认'}}); }
  17. return route.fulfill({json:{status:'ok',entry_id:'entry1'}});
  18. }
  19. if (url.pathname === '/api/data-quality') return route.fulfill({json:{data_version:'v',total:0,datasets:{}}});
  20. if (url.pathname === '/api/runtime') return route.fulfill({json:{data_version:'v',schema_version:'s'}});
  21. if (url.pathname.startsWith('/output/')) return route.fulfill({json:{nodes:[],links:[],relations:[],meta:{}}});
  22. const file = path.join(root, url.pathname === '/' ? 'knowledge_graph_3d.html' : url.pathname);
  23. if (!file.startsWith(root) || !fs.existsSync(file)) return route.fulfill({status:404,body:''});
  24. return route.fulfill({body:fs.readFileSync(file),contentType:file.endsWith('.js')?'application/javascript':file.endsWith('.css')?'text/css':'text/html'});
  25. });
  26. await page.goto('http://qa.test/');
  27. assert.ok((await page.locator('#chatFrame').getAttribute('src')).includes('20260909-timing'));
  28. const frame = await (await page.locator('#chatFrame').elementHandle()).contentFrame();
  29. await frame.waitForFunction(() => typeof askStream === 'function' && Boolean(window.AnswerCacheUI));
  30. await frame.evaluate(async () => {
  31. const original=window.fetch;
  32. const payload={status:'ok',answer:'## 员工人数\n\n共有2名员工。',elapsed_sec:.01,
  33. cache_review:{eligible:true,answer_id:'answer1',checkpoint_id:'checkpoint1',cached:false,hit:false}};
  34. const sse='event: answer_chunk\ndata: '+JSON.stringify({text:'处理中'})+'\n\n' +
  35. 'event: done\ndata: '+JSON.stringify(payload)+'\n\n';
  36. window.fetch=async () => new Response(sse);
  37. try {await askStream('员工有多少人',true,true);} finally {window.fetch=original;}
  38. });
  39. assert.equal(requests.length,0,'rendering an answer must not cache it');
  40. assert.equal(await frame.locator('.answer-timing').last().innerText(),'回答耗时 0.01 秒');
  41. const button=frame.locator('.answer-cache-button').last();
  42. await button.click();
  43. await frame.waitForFunction(() => document.querySelector('.answer-cache-button')?.textContent==='撤销缓存');
  44. assert.equal(await frame.locator('.answer-timing').last().innerText(),'回答耗时 0.01 秒');
  45. assert.equal(requests.length,1);
  46. assert.equal(requests[0].method,'POST');
  47. assert.deepEqual(requests[0].body,{answer_id:'answer1',checkpoint_id:'checkpoint1',entry_id:null});
  48. const originalThreadPath=requests[0].path;
  49. await frame.evaluate(() => { sessionStorage.setItem('kg_thread_id','new-thread'); });
  50. await button.click();
  51. await frame.waitForFunction(() => document.querySelector('.answer-cache-button')?.textContent==='答案正确,加入缓存');
  52. assert.equal(requests[1].method,'DELETE');
  53. assert.equal(requests[1].path,originalThreadPath,'old answer keeps its own thread scope');
  54. assert.equal(requests[1].body.entry_id,'entry1');
  55. failNext=true;
  56. await button.click();
  57. await frame.waitForFunction(() => document.querySelector('.answer-cache-status').textContent.includes('数据已更新'));
  58. assert.equal(await button.innerText(),'答案正确,加入缓存');
  59. assert.equal(await button.isEnabled(),true);
  60. await button.click();
  61. await frame.waitForFunction(() => document.querySelector('.answer-cache-button').textContent==='撤销缓存');
  62. // Resume rendering gets the same explicit approval control.
  63. await frame.evaluate(async () => {
  64. const original=window.fetch;
  65. window.fetch=async () => new Response(JSON.stringify({status:'ok',answer:'确认后完成',elapsed_sec:1.25,
  66. cache_review:{eligible:true,answer_id:'answer2',checkpoint_id:'checkpoint2',cached:false}}));
  67. try {await resumeThread('确认');} finally {window.fetch=original;}
  68. });
  69. assert.equal(await frame.locator('.answer-cache-button').count(),2);
  70. assert.deepEqual(await frame.locator('.answer-timing').allTextContents(),['回答耗时 0.01 秒','回答耗时 1.25 秒']);
  71. await frame.evaluate(() => {
  72. const bubble=appendMessage('assistant','历史确认答案');
  73. AnswerCacheUI.attach(bubble,{cache_review:{eligible:true,answer_id:'hit',checkpoint_id:'cphit',
  74. cached:true,hit:true,entry_id:'entry1'}},'t1',apiBase());
  75. const incomplete=appendMessage('assistant','不完整结果');
  76. AnswerCacheUI.attach(incomplete,{cache_review:{eligible:false,reason:'结果截断'}},'t1',apiBase());
  77. });
  78. assert.equal(await frame.getByText('已复用你确认的历史答案',{exact:true}).count(),1);
  79. assert.equal(await frame.getByText('暂不缓存:结果截断',{exact:true}).count(),1);
  80. assert.equal(await frame.locator('.answer-cache-button').count(),3);
  81. await page.setViewportSize({width:390,height:844});
  82. await frame.locator('.answer-cache-button').last().scrollIntoViewIfNeeded();
  83. assert.ok(await frame.locator('.answer-cache-button').last().isVisible());
  84. console.log('PASS root iframe: explicit approval only, bound answer/checkpoint/thread, revoke, stale-error retry, resume, hit badge, excluded answer, mobile');
  85. } finally {await browser.close();}
  86. })().catch(e=>{console.error(e);process.exitCode=1;});