// Connect to a separate Chromium/Edge instance with remote debugging enabled. // Usage: node tests/browser_smoke.mjs [port=9333] [reset|fast|mid|legacy|axis|black|ui|all] import assert from 'node:assert/strict'; import {setTimeout as sleep} from 'node:timers/promises'; const port=Number(process.argv[2]||9333),selection=process.argv[3]||'all'; const url=new URL('../index.html?test',import.meta.url).href; const response=await fetch(`http://127.0.0.1:${port}/json/list`); assert.ok(response.ok,'Remote debugging endpoint unavailable'); const pages=(await response.json()).filter(t=>t.type==='page'); assert.equal(pages.length,1,'Use one dedicated test tab'); const target=pages[0],ws=new WebSocket(target.webSocketDebuggerUrl); await new Promise((resolve,reject)=>{ws.addEventListener('open',resolve,{once:true});ws.addEventListener('error',reject,{once:true})}); let serial=0;const pending=new Map(),errors=[]; ws.addEventListener('message',event=>{ const m=JSON.parse(event.data); if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text); const p=pending.get(m.id);if(!p)return;pending.delete(m.id);clearTimeout(p.timer); if(m.error)p.reject(new Error(JSON.stringify(m.error)));else p.resolve(m.result); }); function command(method,params={}){ const id=++serial; return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(new Error(method+' timeout'))},240000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))}); } async function evaluate(expression){ const result=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true}); if(result.exceptionDetails)throw new Error(result.exceptionDetails.exception?.description||result.exceptionDetails.text); return result.result.value; } const cases=[ {id:'reset',re:'-0.5',im:'0',span:'3.4',renderMode:'fast',backend:'direct'}, {id:'fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'fast',backend:'fast-extended'}, {id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000001',renderMode:'fast',backend:'fast-extended'}, {id:'legacy',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'accurate',backend:'fast-extended'}, {id:'axis',re:'-0.75',im:'0',span:'0.02',renderMode:'fast',backend:'direct'}, {id:'black',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,fixed:true,renderMode:'accurate',backend:'fast-extended'} ]; assert.ok(['all','kernels','cancel','bench','budget','ui'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case'); // Execute the real production kernels on small, fully specified states. // This isolates terminal and queue behavior from render convergence heuristics. async function kernelCases(){ const {renderer:r}=globalThis.__MANDEL_TEST__.kernelAccess(); await r.ensureDeepActivePipelines(); const d=r.device,B=GPUBufferUsage,created=[],reports=[]; const check=(value,message)=>{if(!value)throw new Error(message)}; const buffer=(size,usage)=>{const b=d.createBuffer({size:Math.max(4,size),usage});created.push(b);return b}; async function read(b,bytes){const staging=buffer(bytes,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(b,0,staging,0,bytes);d.queue.submit([e.finish()]);await staging.mapAsync(GPUMapMode.READ);const out=staging.getMappedRange().slice(0);staging.unmap();return out} async function run({count=1,target=3,chunk=1,passes=3,orbit=[0,0,0,0],error=0,m=0,n=0}){ const capacity=Math.max(1,count),params=new ArrayBuffer(96),p=new DataView(params); [capacity,1,capacity,1,0,0,150000,orbit.length-1,0,0,capacity,0].forEach((v,i)=>p.setUint32(i*4,v,true)); p.setInt32(52,0,true);p.setUint32(84,target,true);p.setUint32(88,chunk,true); const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,params); const refs=buffer(orbit.length*16,B.STORAGE|B.COPY_DST),refData=new Float32Array(orbit.length*4);orbit.forEach((v,i)=>refData[i*4]=v);d.queue.writeBuffer(refs,0,refData); const states=buffer(capacity*32,B.STORAGE|B.COPY_DST|B.COPY_SRC),initial=new ArrayBuffer(capacity*32),sv=new DataView(initial); for(let i=0;ibuffer(capacity*4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),indirect=buffer(12,B.STORAGE|B.INDIRECT|B.COPY_DST); if(count)d.queue.writeBuffer(queues[0],0,Uint32Array.from({length:count},(_,i)=>i));d.queue.writeBuffer(counts[0],0,Uint32Array.of(count)); const meta=buffer(capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buffer(capacity*4,B.STORAGE|B.COPY_SRC); const e=d.createCommandEncoder();let input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input:0,passes,refsB:refs});d.queue.submit([e.finish()]); const active=new Uint32Array(await read(counts[input],4))[0],field=new Uint32Array(await read(meta,capacity*4)); const live=active?Array.from(new Uint32Array(await read(queues[input],active*4))).sort((a,b)=>a-b):[]; return{active,field,live,states:new DataView(await read(states,capacity*32)),async resume(nextTarget,nextPasses){p.setUint32(84,nextTarget,true);d.queue.writeBuffer(pb,0,params);const e=d.createCommandEncoder();input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input,passes:nextPasses,refsB:refs});d.queue.submit([e.finish()]);return{active:new Uint32Array(await read(counts[input],4))[0],field:new Uint32Array(await read(meta,capacity*4))}}}; } try{ let x=await run({orbit:[0,1,2,5]});check(x.active===0&&x.field[0]===(1<<28|3),'escape exactly at target'); x=await run({target:1,n:1,m:1,passes:1,error:.002});check(x.active===0&&x.field[0]===(1<<20|1),'terminal error guard'); x=await run({target:2,passes:2,orbit:[0]});check(x.active===0&&x.field[0]===(3<<20),'reference exhaustion'); x=await run({target:1,passes:1,orbit:[0],m:1});check(x.active===0&&x.field[0]===(3<<20),'reference index guard'); for(const count of [0,1,63,64,65])for(const passes of [1,2,3,4]){ const target=passes;x=await run({count,target,passes,orbit:Array(10).fill(0)}); check(x.active===count,'queue count');check(x.live.every((v,i)=>v===i),'queue membership'); for(let i=0;ii%7===6?1<<28:(i%7)<<20|i); const p=new Uint32Array(24);p.set([9,3,width,height,3,1,100,0,0,2,width,width+3]); const pb=buffer(96,B.UNIFORM|B.COPY_DST),meta=buffer(n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),buckets=buffer(96,B.STORAGE|B.COPY_DST),stats=buffer(32,B.STORAGE|B.COPY_SRC|B.COPY_DST),queue=buffer(n*4,B.STORAGE|B.COPY_SRC),indirect=buffer(16,B.STORAGE|B.INDIRECT); d.queue.writeBuffer(pb,0,p);d.queue.writeBuffer(meta,0,initial);const e=d.createCommandEncoder(); r.encodeDeepBucketHistogram(e,{pbuf:pb,meta,bucketState:buckets,w:9,h:3});r.encodeDeepBucketPrefix(e,{bucketState:buckets,queueStats:stats,indirect});r.encodeDeepBucketScatter(e,{pbuf:pb,meta,bucketState:buckets,queueStats:stats,queue,w:9,h:3});d.queue.submit([e.finish()]); const q=new Uint32Array(await read(stats,32)),expected=[]; for(let y=1;y<4;y++)for(let x=3;x<12;x++){const i=y*width+x;if(i%7>=1&&i%7<=5)expected.push(i)} check(q[0]===expected.length&&q[1]===0&&q[2]===expected.length,'tile queue counts'); const actual=Array.from(new Uint32Array(await read(queue,q[0]*4))).sort((a,b)=>a-b);check(JSON.stringify(actual)===JSON.stringify(expected),'tile queue exact membership'); if(!r.precisionScatter)r.precisionScatter=await r.makeCompute(await r.module('precision-scatter-test',globalThis.MANDEL_WEBGPU_KERNELS.PRECISION_SCATTER_WGSL)); const results=new Uint32Array(n*4);for(let i=0;i=1&&i%7<=5?(1<<28|7):initial[i]),'scatter preserves completed/operation-limit pixels'); } { const pb=buffer(96,B.UNIFORM|B.COPY_DST),p=new Uint32Array(24);p.set([9,1,9,1,0,0,150000,4,0,3,9,0]);p[21]=3;p[22]=1;d.queue.writeBuffer(pb,0,p); const stateB=buffer(3*40,B.STORAGE|B.COPY_SRC),queues=[0,1].map(()=>buffer(12,B.STORAGE|B.COPY_SRC|B.COPY_DST)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_SRC|B.COPY_DST)),indirect=buffer(12,B.STORAGE|B.INDIRECT),refsB=buffer(5*16,B.STORAGE),meta=buffer(9*4,B.STORAGE|B.COPY_SRC),sm=buffer(9*4,B.STORAGE); d.queue.writeBuffer(queues[0],0,Uint32Array.of(8,2,5));d.queue.writeBuffer(counts[0],0,Uint32Array.of(3)); const e=d.createCommandEncoder();r.encodeDeepActiveResumeInit(e,{pbuf:pb,stateB,queue:queues[0],count:3,pipeline:r.deepCompactInit}); const output=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB,queues,counts,indirect,meta,smooth:sm,input:0,passes:3,refsB,pipeline:r.deepCompactContinue});d.queue.submit([e.finish()]); const field=new Uint32Array(await read(meta,36)),states=new DataView(await read(stateB,120)); for(let i=0;i<9;i++)check(field[i]===([8,2,5].includes(i)?6<<20|3:0),'compact pixel mapping'); for(let i=0;i<3;i++){check(states.getUint32(i*40+32,true)===[8,2,5][i],'compact slot identity');check(states.getUint32(i*40+20,true)===3,'compact state retained')} check(new Uint32Array(await read(counts[output],4))[0]===3,'compact queue retained'); } reports.push('tile failure queue exact membership; scatter ownership; compact slot identity'); { const width=17,height=5,n=width*height,whole=buffer(n*4,B.STORAGE|B.COPY_SRC),pieces=buffer(n*4,B.STORAGE|B.COPY_SRC),sm=buffer(n*4,B.STORAGE); const unit=1n<<256n,snap={bits:256,re:-unit/2n,im:0n,span:34n*unit/10n},e=d.createCommandEncoder(); const encode=(meta,x,y,w,h)=>{const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,r.directParams(w,h,width,height,x,y,100,snap,.5,.5,0,width,y*width+x));r.encodeDirectNumeric(e,{pbuf:pb,meta,smooth:sm,w,h})}; encode(whole,0,0,width,height); for(let y=0;yv!==0&&v===actual[i]),'direct tiles reproduce whole-frame coordinates'); } reports.push('direct tile coordinates and full pixel coverage'); return reports; }finally{await d.queue.onSubmittedWorkDone();for(const b of created)b.destroy()} } // Read the actual texture selected by presentation, including animation during // numeric work. UI text alone cannot prove that colors are changing. async function displayedColorHash(){ const r=globalThis.__MANDEL_TEST__.kernelAccess().renderer,f=r.frame,d=r.device; const texture=r.colorSourceVisible?r.colorSource.texture:f.front; const pitch=Math.ceil(f.w*4/256)*256,b=d.createBuffer({size:pitch*f.h,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}); try{ const e=d.createCommandEncoder();e.copyTextureToBuffer({texture},{buffer:b,bytesPerRow:pitch},[f.w,f.h]);d.queue.submit([e.finish()]); await b.mapAsync(GPUMapMode.READ);const data=new Uint8Array(b.getMappedRange()),pixels=new Uint8Array(f.n*4); for(let y=0;yx.toString(16).padStart(2,'0')).join(''); }finally{b.destroy()} } try{ await command('Runtime.enable'); await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false}); await command('Page.navigate',{url}); for(let i=0;i<100;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(100)} const ready=await evaluate('globalThis.__MANDEL_TEST__.ensureGpuReady()'); assert.equal(ready.ready,true,JSON.stringify(ready)); if(selection==='ui'||selection==='all'){ const initial=await evaluate('__MANDEL_TEST__.waitForNumericComplete()'); assert.equal(initial.renderClock.status,'complete'); assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs); if(initial.previewMs)assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs+100,'include refinement delay'); assert.equal(await evaluate('!!document.querySelector("#renderMode")'),false); const original=await evaluate('__MANDEL_TEST__.fieldHashes()'); const originalColor=await evaluate(`(${displayedColorHash.toString()})()`); await evaluate('document.querySelector("#colorAuto").click()'); await sleep(350); assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),originalColor,'animation changes GPU pixels'); assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),original,'animation preserves numeric data'); const idle=await evaluate('__MANDEL_TEST__.state()'); assert.equal(idle.cycle,initial.cycle);assert.equal(idle.lastRender,initial.lastRender); await evaluate('document.querySelector("#colorAuto").click()'); await sleep(150); const stopped=await evaluate('__MANDEL_TEST__.state().shift');await sleep(150); assert.equal(await evaluate('__MANDEL_TEST__.state().shift'),stopped); assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSource'),false,'release animation memory'); await evaluate('document.querySelector("#colorAuto").click()'); const ongoing=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`); let active=false; for(let i=0;i<1000;i++){ const state=await evaluate('__MANDEL_TEST__.state()'); if(state.rendering&&state.provisional&&state.engine.includes('数値補修')){active=true;break} await sleep(20); } assert.ok(active,'animate during expensive repair'); const before=await evaluate(`(${displayedColorHash.toString()})()`); await sleep(350); assert.equal(await evaluate('__MANDEL_TEST__.state().rendering'),true); assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSourceVisible'),true); assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),before,'color changes while numeric work is running'); assert.match(await evaluate('document.querySelector("#render").textContent'),/^描画中… /); // A manual change made while rendering must be drained at completion. await evaluate('document.querySelector("#colorAuto").click();const shift=document.querySelector("#shift");shift.value="0.65";shift.dispatchEvent(new Event("input",{bubbles:true}))'); await ongoing;const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete()'); for(let i=0;i<100;i++){if(await evaluate('!__MANDEL_TEST__.state().recolorPending&&!__MANDEL_TEST__.state().recoloring'))break;await sleep(20)} assert.equal(final.renderClock.status,'complete');assert.ok(final.lastRender>2000); assert.ok(final.lastRender>=final.refineMs); const timing=await evaluate('({label:document.querySelector("#render").textContent,now:performance.now(),state:__MANDEL_TEST__.state(),front:__MANDEL_TEST__.kernelAccess().renderer.frontColorKey})'); assert.equal(timing.label,(final.lastRender/1000).toFixed(2)+' s'); assert.ok(timing.now-final.renderClock.startedAt-final.lastRender<1000,'time is current without starting another render'); assert.equal(Number(timing.front.split(':')[2]),0.65);assert.equal(timing.state.recolorPending,false); const hash=await evaluate('__MANDEL_TEST__.fieldHashes()'); const view={...cases[1],bits:448,baseIter:512,adaptive:false}; await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify(view)})`);await evaluate('__MANDEL_TEST__.waitForNumericComplete()'); assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),hash,'animated and ordinary renders agree'); // Old preferences and shared URLs migrate to the sole supported renderer. const legacyUrl=await evaluate('localStorage.setItem("mandelbrot.renderMode","accurate");const u=new URL(location.href),p=new URLSearchParams(u.hash.slice(1));p.set("rm","accurate");u.hash=p.toString();u.href'); await command('Page.navigate',{url:legacyUrl});await command('Page.reload');await sleep(150); await evaluate('__MANDEL_TEST__.waitForNumericComplete()'); assert.equal(await evaluate('__MANDEL_TEST__.state().renderMode'),'fast'); assert.equal(await evaluate('localStorage.getItem("mandelbrot.renderMode")'),null); console.log(JSON.stringify({ui:'PASS: animation idle/during repair, stop/manual color, numeric equality, total time, legacy migration',renderMs:final.lastRender})); } if(selection==='kernels'||selection==='all'){ await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()'); console.log(JSON.stringify({kernels:await evaluate(`(${kernelCases.toString()})()`)})); } for(const scenario of cases.filter(c=>selection==='all'||c.id===selection)){ const small=['axis','black'].includes(scenario.id); await command('Emulation.setDeviceMetricsOverride',{width:small?64:320,height:small?48:240,deviceScaleFactor:1,mobile:false}); const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario}; const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete({timeout:180000});return {state,hashes:await t.fieldHashes(),diagnostics:t.gpuDiagnostics()}})()`); assert.equal(result.state.numericalFailures,0); assert.equal(result.state.frontierConverged,true); assert.equal(result.state.historyReady,true); assert.ok(result.state.backend.endsWith(scenario.backend),result.state.backend); assert.ok(result.state.frontierMaxDispatchWork<=4000000); assert.equal(result.diagnostics.lossReason,''); assert.deepEqual(result.diagnostics.uncapturedErrors,[]); assert.equal(result.diagnostics.compilation.flatMap(c=>c.messages).filter(m=>m.type==='error').length,0); console.log(JSON.stringify({case:scenario.id,backend:result.state.backend,iter:result.state.effectiveIter,ms:result.state.lastRender,hashes:result.hashes})); if(scenario.id!=='reset'){ const n=result.hashes.pixels,indices=Array.from(new Set([0,n-1,Math.floor(n/2),...Array.from({length:9},(_,i)=>(i*7919+127)%n)])); const samples=await evaluate(`globalThis.__MANDEL_TEST__.independentSamples(${JSON.stringify(indices)})`); let maxSmoothError=0; for(const p of samples){ assert.equal(p.accepted,1,'independent sample inconclusive'); assert.equal((p.packed>>>28)===1,(p.oracle>>>28)===1,JSON.stringify(p)); if((p.oracle>>>28)===1){ assert.equal(p.packed&0xfffff,p.oracle&0xfffff,JSON.stringify(p)); // WGSL reconstructs magnitude and evaluates log2 in f32. Compare // smoothing in representable units, separately from exact escape n. const ulp=2**(Math.floor(Math.log2(Math.max(1,Math.abs(p.oracleSmooth))))-23); const error=Math.abs(p.smoothed-p.oracleSmooth);maxSmoothError=Math.max(maxSmoothError,error); // Keep ULP checks for the established FAST/axis samples. The new // cap case and former Deep-only black case use approximate smoothing // in the unified renderer; classification and escape n remain exact // checks, and their smoothing deviations are explicitly reported. if(!['mid','black'].includes(scenario.id))assert.ok(error<=8*ulp,JSON.stringify(p)); } } console.log(JSON.stringify({case:scenario.id,independentSamples:samples.length,maxSmoothError})); if(scenario.id==='mid'){ const cap=await evaluate('(async()=>{const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let below=0,remaining=0;for(const p of field.meta){if((p>>>28)===0){remaining++;if((p>>>20)!==6||(p&0xfffff)!==150000)below++}}return {below,remaining,png:await __MANDEL_TEST__.pngRoundTrip()}})()'); assert.equal(result.state.effectiveIter,150000);assert.equal(cap.below,0);assert.ok(cap.remaining>0);assert.equal(cap.png.mismatches,0); } } if(scenario.id==='reset'){ // Returning to the same view must reproduce both numerical buffers. await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;await t.panPixels(8,0);await t.waitForNumericComplete();await t.panPixels(-8,0);await t.waitForNumericComplete()})()'); assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),result.hashes); const exports=await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;return [await t.smokeExportTile({w:32,h:24}),await t.smokeExportTile({w:32,h:24,ss:2})]})()'); for(const item of exports){assert.equal(item.length,item.expected);assert.ok(item.checksum)} const png=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip()'); assert.equal(png.mismatches,0);assert.equal(png.width,320);assert.equal(png.height,240); console.log(JSON.stringify({png})); } } if(selection==='all'||selection==='budget'){ await command('Emulation.setDeviceMetricsOverride',{width:64,height:48,deviceScaleFactor:1,mobile:false}); for(const baseIter of [512,4096]){ const view={...cases[3],bits:448,baseIter,adaptive:false,continuationBudget:4096}; const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete();const r=t.kernelAccess().renderer,field=await r.readFieldAll();let below=0,above=0;for(const p of field.meta){if((p>>>28)===0&&(p>>>20)===6&&(p&0xfffff)!==4096)below++;if((p&0xfffff)>4096)above++}return {state,below,above,png:await t.pngRoundTrip()}})()`); assert.equal(result.state.effectiveIter,4096);assert.equal(result.state.continuationBudget,4096);assert.equal(result.below,0);assert.equal(result.above,0);assert.equal(result.png.mismatches,0);assert.equal(result.png.iter,4096); } console.log('PASS: explicit finite budget, all retained pixels reach target, PNG uses that budget'); } if(selection==='all'||selection==='cancel'){ await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false}); const normal={...cases[0],bits:448,baseIter:512,adaptive:false}; await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`); const expected=await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'); const ongoing=evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`).then(value=>({value}),error=>({error:String(error)})); let during=false; for(let i=0;i<1000;i++){ const s=await evaluate('globalThis.__MANDEL_TEST__.state()'); if(s.provisional&&(s.engine.includes('数値補修')||s.gpuStage==='precision scatter')){during=true;break} await sleep(20); } assert.ok(during,'exercise cancellation during CPU repair, not after completion'); const provisionalPng=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip().then(()=>false,()=>true)'); assert.equal(provisionalPng,true,'unfinished field must not enter complete PNG path'); await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`); await ongoing; await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()'); assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),expected); assert.deepEqual((await evaluate('globalThis.__MANDEL_TEST__.gpuDiagnostics()')).uncapturedErrors,[]); console.log('PASS: cancel during CPU repair, preserve newest frame, reject provisional PNG'); } assert.deepEqual(errors,[]); if(selection==='bench'){ await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()'); for(const scenario of cases.filter(c=>['reset','fast'].includes(c.id))){ let expected; for(let run=0;run<4;run++){ const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario}; const record=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__,before=t.efficiency();await t.setView(${JSON.stringify(view)});const s=await t.waitForNumericComplete(),after=t.efficiency(),metrics={};for(const k of Object.keys(after))metrics[k]=k==='activeGpuBytes'?after[k]:after[k]-before[k];return {ms:s.lastRender,iter:s.effectiveIter,numericalFailures:s.numericalFailures,operationLimit:s.unknownReasons.operationLimit,firstDisplayMs:s.generationMetrics.at(-1)?.firstDisplayMs,metrics,hashes:await t.fieldHashes()}})()`); if(expected)assert.deepEqual(record.hashes,expected,'repeated view changed numeric output');expected=record.hashes; console.log(JSON.stringify({benchmark:scenario.id,run,warmup:run===0,...record})); } } } // Ordinary visits should neither expose diagnostics nor invoke legacy gates. await command('Page.navigate',{url:new URL('../index.html',import.meta.url).href}); for(let i=0;i<100;i++){if(await evaluate("!!globalThis.MANDEL_WEBGPU_KERNELS && !!document.querySelector('#engine')"))break;await sleep(100)} assert.equal(await evaluate("typeof globalThis.__MANDEL_TEST__"),'undefined'); console.log(`PASS: ${selection}; normal startup exposes no diagnostics`); }catch(error){ try{console.error(JSON.stringify(await evaluate('({state:globalThis.__MANDEL_TEST__?.state(),gpu:globalThis.__MANDEL_TEST__?.gpuDiagnostics()})')))}catch{} throw error; }finally{ try{await command('Page.navigate',{url:'about:blank'})}catch{} for(const p of pending.values())clearTimeout(p.timer); ws.close(); }