| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 |
- // Public hashes + authenticated read-only browser acceptance. No business mutations.
- import fs from 'node:fs';
- import assert from 'node:assert/strict';
- import {createHash} from 'node:crypto';
- import {chromium} from 'playwright';
- const env={};for(const line of fs.readFileSync('/Users/mac/.config/xiaoshu/legacy-sync-nightly.env','utf8').split('\n')){const m=line.match(/^(?:export\s+)?(XIAOSHU_[A-Z0-9_]+)=(.*)$/);if(m)env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');}
- const origin='https://admin.xiaoshu.pro',base='https://server.xiaoshu.pro',token=env.XIAOSHU_SYNC_OPERATOR_TOKEN;
- const manifest=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
- const sha=data=>createHash('sha256').update(data).digest('hex');
- async function get(url){const r=await fetch(url,{signal:AbortSignal.timeout(30000)});assert.ok(r.ok,'HTTP '+r.status);return r;}
- const current=await(await get(origin+'/deployment-version.json')).json();assert.equal(current.releaseId,manifest.releaseId);assert.equal(current.commit,manifest.commit);
- for(let i=0;i<manifest.files.length;i+=6)await Promise.all(manifest.files.slice(i,i+6).map(async f=>assert.equal(sha(Buffer.from(await(await get(origin+'/'+f.path)).arrayBuffer())),f.sha256,'Public hash mismatch: '+f.path)));
- const selectors={dashboard:'.overview-metrics',members:'app-operations-users tbody tr',stores:'app-operations-stores tbody tr',schedule:'.content-card[aria-busy="false"]',coaches:'app-operations-coaches tbody tr',payroll:'.payroll-summary-table tbody tr',lessons:'app-lesson-records tbody tr','learning-reports':'app-operations-learning-reports tbody tr','support-requests':'.request-list',vocabulary:'app-operations-vocabulary tbody tr','special-training':'.special-card'};
- const operations={dashboard:'ops/dashboard/summary',members:'ops/users/list',stores:'ops/stores/list',schedule:'ops/schedule/calendar',coaches:'ops/coaches/list',payroll:'ops/payroll/overview',lessons:'ops/lessons/list','learning-reports':'ops/learning-reports/list','support-requests':'ops/support-requests/list',vocabulary:'ops/vocabulary/tree','special-training':'ops/special-training/list'};
- const indexHash=manifest.files.find(f=>f.path==='index.html').sha256;
- for(const page of ['login',...Object.keys(selectors)])assert.equal(sha(Buffer.from(await(await get(origin+'/admin/'+page)).arrayBuffer())),indexHash,'Deep route: '+page);
- console.log(JSON.stringify({publicAssets:manifest.files.length,deepRoutes:12,releaseId:manifest.releaseId}));
- const meResponse=await fetch(base+'/parse/users/me',{headers:{'X-Parse-Application-Id':env.XIAOSHU_PARSE_APP_ID,'X-Parse-Session-Token':token}}),me=await meResponse.json();assert.ok(me.objectId,'Operator session invalid');
- const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}),pages=[];
- try{
- for(const [name,selector] of Object.entries(selectors)){
- const ctx=await browser.newContext({viewport:{width:1440,height:1000}}),page=await ctx.newPage(),errors=[],records=[],jobs=[];
- await ctx.addInitScript(auth=>localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth)),{objectId:me.objectId,sessionToken:token,username:'上线验收',roles:me.roles,company:me.company,operationsRole:'ops-manager',isSuperAdmin:true});
- const allowed=new Set([...Object.values(operations),'ops/vocabulary/list','ops/live/versions','ops/payroll/list','ops/pay-rates/list','ops/teaching-rules/list','ops/payroll/coaches']);
- await page.route('**/*',async route=>{const req=route.request();if(req.method()==='POST'&&req.url().includes('/api/functions/')){const op=req.postDataJSON()?.params?.operation;if(!allowed.has(op)){errors.push('Blocked unexpected operation');await route.abort();return;}}await route.continue();});
- page.on('pageerror',()=>errors.push('Browser runtime error'));
- const start=performance.now();
- page.on('response',res=>{const req=res.request();if(req.method()!=='POST'||!req.url().includes('/api/functions/'))return;jobs.push((async()=>{try{const body=await res.json(),params=req.postDataJSON()?.params||{};records.push({operation:params.operation,displayOnly:params.displayOnly===true,summaryOnly:params.summaryOnly===true,lineCoachId:params.lineCoachId,success:res.ok()&&body.success===true,data:body.data});}catch{errors.push('Invalid API response');}})());});
- const primary=page.waitForResponse(res=>res.request().method()==='POST'&&res.request().postDataJSON()?.params?.operation===operations[name],{timeout:60000});
- const words=name==='vocabulary'?page.waitForResponse(res=>res.request().method()==='POST'&&res.request().postDataJSON()?.params?.operation==='ops/vocabulary/list',{timeout:60000}):null;
- await page.goto(origin+'/admin/'+name,{waitUntil:'domcontentloaded'});await(await primary).finished();if(words)await(await words).finished();await page.locator(selector).first().waitFor({state:'visible',timeout:60000});await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));
- const readyMs=Math.round(performance.now()-start);let verificationDisabled;
- if(name==='payroll')await page.waitForFunction(()=>!document.querySelector('.payroll-source-row')?.textContent?.includes('正在核验结算资格'),null,{timeout:60000});
- await Promise.all(jobs);assert.deepEqual(errors,[]);assert.ok(records.length&&records.every(r=>r.success),'Unsuccessful API: '+name);
- const selected=records.filter(r=>r.operation===operations[name]);assert.ok(selected.length);
- let coachDetailMs,exportVerified;
- if(name==='payroll'){
- const summary=selected.at(-1);assert.ok(summary.summaryOnly&&!summary.displayOnly);assert.notEqual(summary.data.sourceStatus.sync.state,'not_checked');assert.equal(summary.data.lines.length,0);
- const exported=await(await fetch(base+'/api/functions/xiaoshu/ops/gateway-v3',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{operation:'ops/payroll/export',month:summary.data.month,summaryOnly:true,lineCoachId:summary.data.coachItems[0]?.coachId}}),signal:AbortSignal.timeout(60000)})).json();assert.equal(exported.success,true);assert.equal(exported.data.summary.payableAmount,summary.data.payableAmount);assert.equal(exported.data.coachItems.length,summary.data.coachItems.length);assert.ok(exported.data.lines.length>0);exportVerified=true;
- verificationDisabled=await page.locator('.create-payroll-action').isDisabled();if(!summary.data.sourceStatus.sync.settlementReady)assert.equal(verificationDisabled,true);
- const coachId=summary.data.coachItems[0]?.coachId;if(coachId){
- const startDetail=performance.now(),response=page.waitForResponse(r=>r.request().method()==='POST'&&r.request().postDataJSON()?.params?.lineCoachId===coachId,{timeout:60000});
- await page.locator('.view-action').first().click();const result=await(await response).json();assert.equal(result.success,true);assert.ok(result.data.lines.every(l=>l.coachId===coachId));assert.notEqual(result.data.sourceStatus.sync.state,'not_checked');
- await page.waitForFunction(()=>!document.querySelector('.payroll-drawer')?.textContent?.includes('正在加载明细'),null,{timeout:60000});assert.equal(await page.locator('.payroll-drawer tbody tr').count(),Math.max(result.data.lines.length,1));coachDetailMs=Math.round(performance.now()-startDetail);
- await page.locator('.payroll-drawer button[aria-label="关闭"]').click();await page.locator('.payroll-drawer').waitFor({state:'detached',timeout:5000});assert.equal(await page.locator('.payroll-drawer').count(),0);
- }
- }
- let menuReturn;
- if(['lessons','support-requests'].includes(name)){await page.locator('#admin-sidebar a[href="/admin/special-training"]').last().click();await page.locator('.special-card').first().waitFor({timeout:30000});await Promise.all(jobs);const count=records.filter(r=>r.operation===operations[name]).length,at=performance.now();await page.locator('#admin-sidebar a[href="/admin/'+name+'"]').last().click();await page.locator(selector).first().waitFor({timeout:30000});await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));await Promise.all(jobs);menuReturn={ms:Math.round(performance.now()-at),primaryRequests:records.filter(r=>r.operation===operations[name]).length-count};assert.equal(menuReturn.primaryRequests,0);}
- const result={page:name,readyMs,passed:true,primaryRequests:selected.length,verificationDisabled,coachDetailMs,exportVerified,menuReturn};pages.push(result);console.log(JSON.stringify(result));await ctx.close();
- }
- }finally{await browser.close();}
- const receipt={at:new Date().toISOString(),origin,releaseId:manifest.releaseId,commit:manifest.commit,filesVerified:manifest.files.length,deepRoutesVerified:12,readOnly:true,measurement:'One production smoke sample per page; not comparable to controlled p75',pages};
- fs.writeFileSync('perf/admin-speed-round2-2026-11-10/release-verification.json',JSON.stringify(receipt,null,2)+'\n');
|