import {startBrowser,browserCase} from './browser-case.mjs'; import {readFileSync,writeFileSync} from 'node:fs'; import {createHash,randomBytes} from 'node:crypto'; import {candidateCode,withTemporaryIncomeView} from './candidate-code.mjs'; const base='https://server.xiaoshu.pro/parse',app=process.env.XIAOSHU_PARSE_APP_ID,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN,master=process.env.XIAOSHU_MASTER_KEY; if(!app||!token||!master)throw Error('Missing authorized configuration'); const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},out='tmp/admin-data-2026-10-15'; async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(20000)}),d=await r.json();if(!r.ok||d.error)throw Error('Setup HTTP '+r.status);return d;} const me=await(await fetch(base+'/users/me',{headers:{'X-Parse-Application-Id':app,'X-Parse-Session-Token':token}})).json();if(!me.objectId)throw Error('Invalid operator'); const operations=process.argv.slice(2).length?process.argv.slice(2):['ops/users/list','ops/stores/list','ops/schedule/calendar','ops/dashboard/summary','ops/payroll/overview']; const saved=JSON.parse(readFileSync(out+'/online-before.json','utf8')),expiry=Date.now()+3600000,paths={},created=[]; // Clock metadata and the dashboard-only settlement-status metadata are omitted. // Dashboard estimates explicitly cannot authorize settlement; money, counts and all other fields stay exact. Row ordering, amounts, permissions and statuses remain exact. function clean(x,path=[]){if(Array.isArray(x))return x.map((v,i)=>clean(v,[...path,String(i)]));if(x&&typeof x==='object')return Object.fromEntries(Object.keys(x).sort().filter(k=>!['refreshedAt','lagSeconds'].includes(k)&&!(path.join('.')==='payroll.sourceStatus'&&['sync','complete','stale'].includes(k))).map(k=>[k,clean(x[k],[...path,k])]));return x;} const hash=x=>createHash('sha256').update(JSON.stringify(clean(x))).digest('hex'); const measurements=[],browser=process.env.PERF_BROWSER?await startBrowser():null; try{ for(const variant of ['A','B']){ const body=variant==='A'?withTemporaryIncomeView(saved.code,!process.env.PERF_CURRENT_VIEW):candidateCode(saved.code,{temporaryView:true}); const guard=`\nconst __original=handler;handler=async function(request,response){if(request.user?.id!==${JSON.stringify(me.objectId)}||Date.now()>${expiry}||!${JSON.stringify(operations)}.includes(request.body?.params?.operation))return response.status(403).json({success:false});return __original(request,response);};`; const path='xiaoshu/system/perf-pair-'+randomBytes(10).toString('hex');const fn=await parse('/classes/Function','POST',{name:path,path,code:body+guard,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'});created.push(fn.objectId);paths[variant]=path; } for(const operation of operations){for(let pair=0;pairr.hash));measurements.push(entry);results.push(entry);continue;}const params={operation,page:1,pageSize:operation==='ops/vocabulary/list'?30:20,search:'',month:'2026-10',dateFrom:'2026-10-15',dateTo:'2026-10-21',source:'database',refresh:true,filters:operation==='ops/vocabulary/list'?{nodeId:0,status:'',missingAudio:false}:{status:'all'}},start=performance.now(); const r=await fetch(base.replace('/parse','/api/functions/')+paths[variant],{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params}),signal:AbortSignal.timeout(90000)});const ttfb=performance.now()-start,raw=await r.text(),data=JSON.parse(raw),elapsed=performance.now()-start;if(!r.ok||data.success===false)throw Error(operation+' '+variant+' failed: '+data.message); if(pair===0)writeFileSync(out+'/pair-'+operation.replaceAll('/','-')+'-'+variant+'.json',raw,{mode:0o600}); const entry={operation,pair,variant,ms:Math.round(elapsed),ttfbMs:Math.round(ttfb),bytes:Buffer.byteLength(raw),hash:hash(data.data)};measurements.push(entry);results.push(entry); } const equal=results[0].hash===results[1].hash;console.log(JSON.stringify({operation,pair,A:results[0].ms,B:results[1].ms,equal}));writeFileSync(out+'/'+(process.env.PERF_OUTPUT||(browser?'paired-browser.json':'paired-api.json')),JSON.stringify(measurements,null,2),{mode:0o600});if(!equal)throw Error('Golden output changed: '+operation); }} }finally{if(browser)await browser.close();for(const id of created)await parse('/classes/Function/'+id,'DELETE');}