perf(report): load side wrinkle layers after report entry

This commit is contained in:
xsl
2026-10-16 16:34:11 +08:00
parent b19811f974
commit 9181069643
8 changed files with 256 additions and 8 deletions
+8 -2
View File
@@ -61,7 +61,12 @@ try {
assert.equal(result.report_saved, true);
assert(result.vm && result.presentation);
assert.equal(result.wrinkle_views.version,'independent-side-wrinkles-v1');
assert.equal(result.wrinkle_views.left.status,'ready');assert.equal(result.wrinkle_views.right.status,'ready');
assert.equal(result.wrinkle_views.left.status,'pending');assert.equal(result.wrinkle_views.right.status,'pending');
assert(result.wrinkle_job.token);
const layers = await (await api.post(base+'api/wrinkle-views',{data:{token:result.wrinkle_job.token}})).json();
assert.equal(layers.ok,true);assert.equal(layers.wrinkle_views.left.status,'ready');assert.equal(layers.wrinkle_views.right.status,'ready');
const cachedLayers = await (await api.post(base+'api/wrinkle-views',{data:{token:result.wrinkle_job.token}})).json();
assert.deepEqual(cachedLayers,layers);
const expectedTotal = Math.round(result.vm.dims.firm.S * 4 + result.vm.dims.vita.S * 3 + result.vm.dims.stab.S * 3);
assert.equal(result.vm.total, expectedTotal);
assert.notEqual(expectedTotal, result.result.score_info.total_score, 'fixture distinguishes weighted score from API total');
@@ -104,7 +109,8 @@ try {
const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json();
assert.equal(snapshot.report.photos.length, 3);
assert.deepEqual(snapshot.report.presentation, result.presentation);
assert.deepEqual(snapshot.report.payload.wrinkle_views,result.wrinkle_views);
assert.deepEqual(snapshot.report.payload.wrinkle_views,layers.wrinkle_views);
assert.equal(snapshot.report.payload.wrinkle_job.status,'complete');
assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps);
assert.equal(snapshot.report.photos[0], fixture.photos[0]);
assert.equal(snapshot.report.photos[1],fixture.photos[0]); // This HTTP test submits the same fixture for all three views.
+55
View File
@@ -0,0 +1,55 @@
// Real HTTP + report UI. All provider calls are delayed in-memory fixtures.
import assert from 'node:assert/strict';
import fs from 'node:fs';import os from 'node:os';import path from 'node:path';
import {spawn,execSync} from 'node:child_process';
import {createRequire} from 'node:module';import {fileURLToPath,pathToFileURL} from 'node:url';
const require=createRequire(import.meta.url),h5=fileURLToPath(new URL('../',import.meta.url));
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'juzi-wrinkle-async-'));
const owner=require('./wechat-fixture.cjs')(path.join(dir,'accounts'))[0];
const {chromium}=await import(pathToFileURL(execSync('npm root -g',{encoding:'utf8'}).trim()+'/playwright/index.mjs').href);
const child=spawn(process.execPath,['-e',`
const fs=require('fs'),vm=require('vm'),path=require('path'),w={};vm.runInNewContext(fs.readFileSync('assets/mock/report-fixture.js','utf8'),{window:w});const f=w.MOCK_FIXTURE;
global.fetch=async(url,o)=>{const side=o.body.get('return_maps')==='texture_enhanced_lines';
fs.appendFileSync(path.join(process.env.JUZI_DATA_ROOT,'provider-calls.jsonl'),JSON.stringify({side})+'\\n');
if(side)await new Promise(r=>setTimeout(r,1000));
return{ok:true,status:200,json:async()=>({result:f.result,face_rectangle:f.face_rectangle,left_side_result:f.side.left,right_side_result:f.side.right})};};require('./server.js');
`],{cwd:h5,env:{...process.env,HOST:'127.0.0.1',PORT:'18142',JUZI_DATA_ROOT:dir,NODE_ENV:'test',SKIN_API_KEY:'fixture',SKIN_API_SECRET:'fixture',PUBLIC_BASE_URL:'',WECHAT_APP_ID:'',WECHAT_APP_SECRET:''},stdio:['ignore','pipe','pipe']});
let browser;
try {
await new Promise((resolve,reject)=>{const t=setTimeout(()=>reject(Error('startup timeout')),10000);child.stdout.on('data',b=>{if(b.toString().includes('已启动')){clearTimeout(t);resolve();}});child.once('exit',()=>{clearTimeout(t);reject(Error('server exit'));});});
const base='http://127.0.0.1:18142/';browser=await chromium.launch({headless:true});const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message));
await page.context().addCookies([{name:'juzi_session',value:owner.token,url:base}]);
const source=fs.readFileSync(path.join(h5,'js/app.js'),'utf8');
await page.route('**/js/app.js*',r=>r.fulfill({contentType:'text/javascript',body:source.replace(/\}\)\(\);\s*$/,`
window.__asyncScreen=showScreen;
window.__startAsyncReport=function(){beginCaptureJourney();['front','right','left'].forEach(trackCapture);photos=Array(3).fill(window.MOCK_FIXTURE.photos[0]);camSeq++;showScreen('s-cam');submitPhotos(camSeq);};})();`)}));
await page.addInitScript(()=>{const f=window.fetch;window.__annotationStarts=[];window.fetch=function(url,options){if(String(url).includes('api/wrinkle-views'))window.__annotationStarts.push({report:!!document.querySelector('#s-report.active'),scan:!!document.querySelector('#p-analyzing.show')});return f.call(this,url,options);};});
await page.goto(base);await page.waitForFunction(()=>AccountClient.user());await page.addScriptTag({url:base+'assets/mock/report-fixture.js'});
const primary=page.waitForResponse(r=>r.url().endsWith('/api/skin-analyze'));await page.evaluate(()=>window.__startAsyncReport());
const result=await(await primary).json();assert.equal(result.ok,true);assert.equal(result.wrinkle_views.left.status,'pending');
assert.equal(fs.readFileSync(path.join(dir,'provider-calls.jsonl'),'utf8').trim().split('\n').length,1,'main response cannot wait for side paid calls');
await page.waitForSelector('#s-report.active');const conclusion=await page.locator('#ditem-para2').textContent();
await page.waitForFunction(()=>document.querySelector('#wrinkle-layer-note').textContent.includes('加载中'));
await page.waitForFunction(()=>window.__annotationStarts.length===1);
await page.evaluate(()=>window.__asyncScreen('s-splash'));
const api=page.context().request;
const cached=await(await api.post(base+'api/wrinkle-views',{data:{token:result.wrinkle_job.token}})).json();assert.equal(cached.ok,true);
await page.waitForTimeout(100);
assert.equal(await page.locator('#s-splash.active').count(),1,'late annotation response cannot navigate');
assert.equal(await page.locator('#wrinkle-left').getAttribute('data-has-annotation'),'false','hidden report is not patched by stale response');
await page.evaluate(()=>window.__asyncScreen('s-report'));
await page.waitForFunction(()=>['left','right'].every(v=>document.querySelector('#wrinkle-'+v).dataset.hasAnnotation==='true'));
assert.equal(await page.locator('#ditem-para2').textContent(),conclusion);
assert.deepEqual(await page.evaluate(()=>window.__annotationStarts),[{report:true,scan:false}]);
const stored=await(await api.get(base+'api/reports/'+result.record_id)).json();assert.equal(stored.report.payload.wrinkle_job.status,'complete');assert.deepEqual(stored.report.vm,result.vm);assert.deepEqual(stored.report.presentation,result.presentation);
const other=await browser.newContext();await other.request.post(base+'api/auth/browser',{data:{consent:true}});assert.equal((await other.request.post(base+'api/wrinkle-views',{data:{token:result.wrinkle_job.token}})).status(),404);await other.close();
assert.equal(fs.readFileSync(path.join(dir,'provider-calls.jsonl'),'utf8').trim().split('\n').length,3,'main + exactly two side calls, cache/access denial never repeat');
assert.equal((await api.get(base+'data/wrinkle-jobs/'+result.wrinkle_job.token+'.json')).status(),403);
await page.route('**/api/wrinkle-views',r=>r.fulfill({status:503,json:{ok:false}}));
await page.evaluate(()=>window.__startAsyncReport());await page.waitForSelector('#s-report.active');
await page.waitForFunction(()=>document.querySelector('#wrinkle-layer-note').textContent.includes('部分侧脸未获取'));
assert.equal(await page.locator('#s-report.active').count(),1,'optional annotation failure keeps report accessible');
assert.equal(await page.locator('#ditem-para2').textContent(),conclusion);
assert.equal(fs.readFileSync(path.join(dir,'provider-calls.jsonl'),'utf8').trim().split('\n').length,4,'failed optional endpoint adds no provider retry');
assert.deepEqual(errors,[]);console.log('PASS: fast main report, side requests only after active report, pending/ready UI, unchanged conclusions/scores, owner isolation, snapshot cache, exactly 3 fixture calls');
} finally {if(browser)await browser.close();child.kill();}
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path');
const create=require('../lib/wrinkle-jobs');
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'juzi-wrinkle-jobs-'));
const owner={id:'owner',kind:'wechat'},guest={id:'guest',kind:'browser'};
const ready={version:'independent-side-wrinkles-v1',left:{status:'ready'},right:{status:'ready'}};
(async()=>{
let calls=0,finish,completed=0;
const deps={dir,collect:()=>{calls++;return new Promise(r=>{finish=r;});},load:()=>({}),complete:()=>{completed++;}};
const jobs=create(deps),job=jobs.register(owner,'Rtest',{});assert.equal(calls,0,'registration cannot start paid work');
assert.equal(await jobs.run({id:'intruder',kind:'wechat'},job.token),null);
const a=jobs.run(owner,job.token),b=jobs.run(owner,job.token);assert.equal(calls,1);finish(ready);
assert.deepEqual(await a,ready);assert.deepEqual(await b,ready);assert.equal(completed,1);
assert.deepEqual(await jobs.run(owner,job.token),ready);assert.equal(calls,1);
const restarted=create({...deps,collect:()=>{throw Error('must use persistent cache');}});
assert.deepEqual(await restarted.run(owner,job.token),ready);
const before=fs.readdirSync(dir);const g=jobs.register(guest,'Rguest',{});
const gr=jobs.run(guest,g.token);finish(ready);await gr;
assert.deepEqual(fs.readdirSync(dir),before,'guest jobs/photos/results never persist');
const pending=jobs.register(owner,'Rpending',{});
const reload=create({...deps,collect:async()=>ready});assert.deepEqual(await reload.run(owner,pending.token),ready,'pending restart reloads own saved snapshot');
const running=jobs.register(owner,'Rrunning',{});const old=jobs.run(owner,running.token);
const afterCrash=create({...deps,collect:()=>{throw Error('must not charge twice');}});
assert.equal((await afterCrash.run(owner,running.token)).left.error_code,'WRINKLE_JOB_INTERRUPTED');finish(ready);await old;
const failed=create({...deps,collect:async()=>{throw Error('provider failed');}});const f=failed.register(owner,'Rfailed',{});
assert.equal((await failed.run(owner,f.token)).left.error_code,'WRINKLE_JOB_FAILED');
assert.equal(await failed.run(owner,'../bad'),null);
console.log('PASS: lazy owner-bound single-flight jobs, durable success/failure cache, crash no-duplicate-charge policy, pending recovery, guest RAM-only');
})().catch(e=>{console.error(e);process.exitCode=1;});