| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- const assert = require('node:assert/strict');
- const fs = require('node:fs');
- const path = require('node:path');
- const {chromium} = require('playwright');
- const root = path.resolve(__dirname, '../html');
- (async () => {
- const browser = await chromium.launch({headless:true,channel:'msedge'});
- try {
- const page = await browser.newPage({viewport:{width:1280,height:900}});
- const requests = [];
- let failNext = false;
- await page.route('**/*', async route => {
- const url = new URL(route.request().url());
- if (url.pathname.endsWith('/answer-cache')) {
- requests.push({method:route.request().method(), path:url.pathname, body:route.request().postDataJSON()});
- if (failNext) { failNext=false; return route.fulfill({status:409,json:{detail:'数据已更新,请重新提问后确认'}}); }
- return route.fulfill({json:{status:'ok',entry_id:'entry1'}});
- }
- if (url.pathname === '/api/data-quality') return route.fulfill({json:{data_version:'v',total:0,datasets:{}}});
- if (url.pathname === '/api/runtime') return route.fulfill({json:{data_version:'v',schema_version:'s'}});
- if (url.pathname.startsWith('/output/')) return route.fulfill({json:{nodes:[],links:[],relations:[],meta:{}}});
- const file = path.join(root, url.pathname === '/' ? 'knowledge_graph_3d.html' : url.pathname);
- if (!file.startsWith(root) || !fs.existsSync(file)) return route.fulfill({status:404,body:''});
- return route.fulfill({body:fs.readFileSync(file),contentType:file.endsWith('.js')?'application/javascript':file.endsWith('.css')?'text/css':'text/html'});
- });
- await page.goto('http://qa.test/');
- assert.ok((await page.locator('#chatFrame').getAttribute('src')).includes('20260909-timing'));
- const frame = await (await page.locator('#chatFrame').elementHandle()).contentFrame();
- await frame.waitForFunction(() => typeof askStream === 'function' && Boolean(window.AnswerCacheUI));
- await frame.evaluate(async () => {
- const original=window.fetch;
- const payload={status:'ok',answer:'## 员工人数\n\n共有2名员工。',elapsed_sec:.01,
- cache_review:{eligible:true,answer_id:'answer1',checkpoint_id:'checkpoint1',cached:false,hit:false}};
- const sse='event: answer_chunk\ndata: '+JSON.stringify({text:'处理中'})+'\n\n' +
- 'event: done\ndata: '+JSON.stringify(payload)+'\n\n';
- window.fetch=async () => new Response(sse);
- try {await askStream('员工有多少人',true,true);} finally {window.fetch=original;}
- });
- assert.equal(requests.length,0,'rendering an answer must not cache it');
- assert.equal(await frame.locator('.answer-timing').last().innerText(),'回答耗时 0.01 秒');
- const button=frame.locator('.answer-cache-button').last();
- await button.click();
- await frame.waitForFunction(() => document.querySelector('.answer-cache-button')?.textContent==='撤销缓存');
- assert.equal(await frame.locator('.answer-timing').last().innerText(),'回答耗时 0.01 秒');
- assert.equal(requests.length,1);
- assert.equal(requests[0].method,'POST');
- assert.deepEqual(requests[0].body,{answer_id:'answer1',checkpoint_id:'checkpoint1',entry_id:null});
- const originalThreadPath=requests[0].path;
- await frame.evaluate(() => { sessionStorage.setItem('kg_thread_id','new-thread'); });
- await button.click();
- await frame.waitForFunction(() => document.querySelector('.answer-cache-button')?.textContent==='答案正确,加入缓存');
- assert.equal(requests[1].method,'DELETE');
- assert.equal(requests[1].path,originalThreadPath,'old answer keeps its own thread scope');
- assert.equal(requests[1].body.entry_id,'entry1');
- failNext=true;
- await button.click();
- await frame.waitForFunction(() => document.querySelector('.answer-cache-status').textContent.includes('数据已更新'));
- assert.equal(await button.innerText(),'答案正确,加入缓存');
- assert.equal(await button.isEnabled(),true);
- await button.click();
- await frame.waitForFunction(() => document.querySelector('.answer-cache-button').textContent==='撤销缓存');
- // Resume rendering gets the same explicit approval control.
- await frame.evaluate(async () => {
- const original=window.fetch;
- window.fetch=async () => new Response(JSON.stringify({status:'ok',answer:'确认后完成',elapsed_sec:1.25,
- cache_review:{eligible:true,answer_id:'answer2',checkpoint_id:'checkpoint2',cached:false}}));
- try {await resumeThread('确认');} finally {window.fetch=original;}
- });
- assert.equal(await frame.locator('.answer-cache-button').count(),2);
- assert.deepEqual(await frame.locator('.answer-timing').allTextContents(),['回答耗时 0.01 秒','回答耗时 1.25 秒']);
- await frame.evaluate(() => {
- const bubble=appendMessage('assistant','历史确认答案');
- AnswerCacheUI.attach(bubble,{cache_review:{eligible:true,answer_id:'hit',checkpoint_id:'cphit',
- cached:true,hit:true,entry_id:'entry1'}},'t1',apiBase());
- const incomplete=appendMessage('assistant','不完整结果');
- AnswerCacheUI.attach(incomplete,{cache_review:{eligible:false,reason:'结果截断'}},'t1',apiBase());
- });
- assert.equal(await frame.getByText('已复用你确认的历史答案',{exact:true}).count(),1);
- assert.equal(await frame.getByText('暂不缓存:结果截断',{exact:true}).count(),1);
- assert.equal(await frame.locator('.answer-cache-button').count(),3);
- await page.setViewportSize({width:390,height:844});
- await frame.locator('.answer-cache-button').last().scrollIntoViewIfNeeded();
- assert.ok(await frame.locator('.answer-cache-button').last().isVisible());
- console.log('PASS root iframe: explicit approval only, bound answer/checkpoint/thread, revoke, stale-error retry, resume, hit badge, excluded answer, mobile');
- } finally {await browser.close();}
- })().catch(e=>{console.error(e);process.exitCode=1;});
|