food_chain/tests/regression.mjs
2026-09-29 23:45:12 +09:00

201 lines
15 KiB
JavaScript

import assert from 'node:assert/strict';
import fsSync from 'node:fs';
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {createHash} from 'node:crypto';
const prodUrl=new URL('../index.html',import.meta.url);
const prodHtml=await fs.readFile(prodUrl,'utf8');
const extract=html=>[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const prodScripts=extract(prodHtml);
assert.equal(prodScripts.length,2);
for(const script of prodScripts)new vm.Script(script);
// Production is the single source of truth. No copied test HTML is allowed.
assert.equal(fsSync.existsSync(new URL('./index.test.html',import.meta.url)),false);
assert.doesNotMatch(prodHtml,/TEST_MODE|__MANDEL_TEST__|\bruntime\b|generationMetric|activeBatchLimit/);
assert.doesNotMatch(prodHtml,/truestate|returnfinishRenderClock/);
// Removed legacy/dead subsystems and stale diagnostics must not creep back.
for(const pattern of [
/DEEP_PERTURB/,/FAILURE_TILE_MAP_WGSL/,/DEEP_COMPACT_(?:INIT|CONTINUE)_WGSL/,
/referenceCandidates\(/,/refinePixelFrontierLegacy/,/refinePixelFrontierFinite/,
/pixelFrontierIterationFloor/,/prefetchFinalReference/,/readMetaAll/,/readFieldAll/,
/renderTileMeta/,/isOperationLimitMeta/,/readUnresolved\(/,
/numericRecoveryPending|numericRecoveryRunning|qualityFinalIter|qualityStage/,
/adaptiveProbeScorer|guidedReferenceScorer/,
/\bFIELD_INTERIOR_LIKELY\b/,/function fromDec\(/,/function decimalRequiredBits\(/,
/state\.(?:lastEngine|frontierRounds|frontierPixels|frontierEscaped|frontierIter|frontierMinimumIter|frontierConverged|gpuUnavailable|qualityIter|refineMs|lastRender|gpuStage)/,
/this\.(?:adapterInfo|compilation|uncapturedErrors)/,
/cpuFrameRgba/,/fast-coverage-repair/,/mode 2 repairs only untouched coverage holes/
]) assert.doesNotMatch(prodHtml,pattern,String(pattern));
// Pin the surviving kernels.
const kc={};vm.runInNewContext(prodScripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
assert.equal(Object.keys(kernels).length,20,'19 WGSL kernels + version');
const expected=JSON.parse(await fs.readFile(new URL('./kernel_hashes.json',import.meta.url),'utf8'));
const hashes=Object.fromEntries(Object.entries(kernels).map(([k,v])=>[k,createHash('sha256').update(v).digest('hex')]));
assert.deepEqual(hashes,expected);
assert.doesNotMatch(kernels.COLOR_WGSL,/reason\s*==\s*6u[^}]*interior_color/s);
assert.match(kernels.COLOR_WGSL,/Finite-budget survivors are unresolved membership/);
assert.match(kernels.COLOR_WGSL,/textureStore\(outTex,vec2<i32>\(gid\.xy\),vec4<f32>\(0\.0\)\)/);
assert.match(kernels.DIRECT_F32_WGSL,/candPeriod/);
assert.match(kernels.DIRECT_F32_WGSL,/FIELD_INTERIOR_HEURISTIC/);
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/pack_meta\(n,FIELD_INTERIOR_HEURISTIC\)/);
assert.match(kernels.FAST_PERTURB_WGSL,/series_start/);
assert.match(prodHtml,/SERIES_APPROXIMATION_ENABLED=false/);
assert.match(prodHtml,/deferPrimaryColor=deferColor\|\|fastExtended/);
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/unknownOnly==2u/);
assert.match(prodHtml,/knownInteriorViewportMayOverlap/);
assert.match(prodHtml,/numericHistoryScaleEligible/);
assert.match(prodHtml,/ensureExportAaSamples/);
assert.match(prodHtml,/exportWorkspaceDestroy\(\);exportJob\.active=false/);
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/pixelMap/);
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/states\[slot\]/);
function appContext(){
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}};
let revoked=0;
const context={
MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},
navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,
URLSearchParams,URL:{createObjectURL:()=>`blob:test-${Math.random()}`,revokeObjectURL:()=>{revoked++}},
TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},
setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:800,innerHeight:600,devicePixelRatio:1,
localStorage:{getItem(){return null},setItem(){},removeItem(){}},
};
const cut=prodScripts[1].indexOf('// ── boot / teardown');
assert.ok(cut>0,'boot marker missing');
const code=prodScripts[1].slice(0,cut)+`\n`+
`globalThis.internal={state,refs,referenceScorer,fastRefs,precisionFallbackPool,ReferenceService,FastReferenceService,PrecisionFallbackService,WebGpuRenderer,chooseBackend,numericalFailureCount,pixelPrecisionBits,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource,adaptiveProbeChoice,adaptiveProbeSources,operationLimitReferenceEligible,failureGuidedReferenceEligible,tryOperationLimitGuidedReference,integerGridReuseShift,nearbyReferenceCandidates,refinePixelFrontier,fixedSourceForPixel,applyQuality,maxIter,buildExportMetadata,cancelRender,exportJob,renderFrame,cpuFallbackWorkerSource,recoverNumericalGuided};})();`;
vm.runInNewContext(code,context);
context.revoked=()=>revoked;
return context;
}
function fromDec(s,bits=256){
s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);
const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',f='']=mant.split('.');
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}
const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v;
}
const c=appContext(),a=c.internal;
// Finite quality presets remain explicit.
for(const [q,budget] of [['fast',4096],['standard',16384],['high',32768]]){a.applyQuality(q);assert.equal(a.state.continuationBudget,budget)}
for(const old of ['fine','validate']){a.applyQuality(old);assert.equal(a.state.continuationBudget,32768)}
// Backend selection remains direct vs FAST only; primary deep routing is gone.
for(const [span,expectedBackend] of [['3.4','direct'],['0.00000000000034','fast-extended']]){
const snap={bits:256,re:fromDec('-0.743643887037151'),im:fromDec('0.13182590420533'),span:fromDec(span)};
assert.equal(a.chooseBackend(snap,800).backend,expectedBackend);
}
assert.doesNotMatch(a.chooseBackend.toString(),/deep/);
// Finite operation limit is not a numerical failure or a membership proof.
assert.equal(a.numericalFailureCount({reasons:{errorBound:1,escapeUncertain:1,referenceEnd:1,rebaseGap:1,range:1,operationLimit:99}}),5);
{
Object.assign(a.state,{continuationBudget:1024,token:7});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
const r={beginGpuOperationLimitContinuation:async()=>({active:5}),readUnresolvedStats:async()=>({total:5,reasons:{operationLimit:5}})};
const result=await a.refinePixelFrontier(r,snap,1024,7,{total:5,reasons:{operationLimit:5}});
assert.equal(result.finiteBudgetComplete,true);assert.equal(result.membershipCertified,false);
}
{
Object.assign(a.state,{continuationBudget:1024,token:8});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
const r={beginGpuOperationLimitContinuation:async()=>({active:0}),readUnresolvedStats:async()=>({total:0,reasons:{operationLimit:0}})};
const result=await a.refinePixelFrontier(r,snap,1024,8,{total:0,reasons:{operationLimit:0}});
assert.equal(result.membershipCertified,true);
}
{
Object.assign(a.state,{continuationBudget:1024,token:9});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
const r={beginGpuOperationLimitContinuation:async()=>({active:0}),readUnresolvedStats:async()=>({total:0,heuristicInterior:1,reasons:{operationLimit:0}})};
const result=await a.refinePixelFrontier(r,snap,1024,9,{total:0,heuristicInterior:1,reasons:{operationLimit:0}});
assert.equal(result.membershipCertified,false);
}
// The 12-point adaptive probe policy stays intact.
{
const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
assert.equal(a.adaptiveProbeSources(snap,800,600).length,12);
assert.equal(a.adaptiveProbeChoice(Array(12).fill(3000),3870,350).iter,3870);
assert.equal(a.adaptiveProbeChoice(Array(12).fill(7000),3870,350).iter,350);
}
// Operation-limit scoring is staged: first 12, optionally second 12, one build.
{
Object.assign(a.state,{token:20,bits:256});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')},stats={reasons:{operationLimit:1000}},current={refLen:100};
const r={sampleActiveIndices:async()=>Uint32Array.from({length:24},(_,i)=>i)};
const oldScore=a.referenceScorer.scoreFixedCandidates,oldBuild=a.referenceScorer.requestFixed;
let scores=0,builds=0;
a.referenceScorer.scoreFixedCandidates=async()=>{scores++;return{winner:0,bestScore:300,precisionBits:160,scoreMs:1}};
a.referenceScorer.requestFixed=async(_s,_i,_w,_h,src)=>{builds++;return{refLen:300,checkpointMismatch:false,source:src,buildMs:1}};
let got=await a.tryOperationLimitGuidedReference(r,{},snap,300,20,stats,current,new Set());
assert.equal(got.used,true);assert.equal(got.usedSecond,false);assert.equal(scores,1);assert.equal(builds,1);
scores=0;builds=0;
a.referenceScorer.scoreFixedCandidates=async()=>{scores++;return scores===1?{winner:0,bestScore:180,precisionBits:160,scoreMs:1}:{winner:0,bestScore:250,precisionBits:160,scoreMs:1}};
a.referenceScorer.requestFixed=async(_s,_i,_w,_h,src)=>{builds++;return{refLen:250,checkpointMismatch:false,source:src,buildMs:1}};
got=await a.tryOperationLimitGuidedReference(r,{},snap,300,20,stats,current,new Set());
assert.equal(got.used,true);assert.equal(got.usedSecond,true);assert.equal(scores,2);assert.equal(builds,1);
a.referenceScorer.scoreFixedCandidates=oldScore;a.referenceScorer.requestFixed=oldBuild;
}
// Exact certificates remain strict at cardioid/bulb boundaries.
{
const one=1n<<256n;
for(const [re,expectedInside] of [[0n,true],[-one,true],[one/4n,false],[one/4n-1n,true],[-5n*one/4n,false],[-5n*one/4n+1n,true]]){
assert.equal(a.certifiedInteriorTiles({bits:256,re,im:0n,span:0n},1,1).pixels===1,expectedInside,re.toString());
}
}
// Stable integer-grid history reuse remains exact.
{
const bits=256,span=(1n<<240n)+12345n,old={bits,re:0n,im:0n,span,w:800,h:600},next={bits,re:-(span*40n)/800n,im:0n,span,w:800,h:600};
const shift=a.integerGridReuseShift(old,next,800,600);assert.ok(shift);assert.equal(shift.pixels,456000);
assert.equal(a.integerGridReuseShift(old,{...next,re:-(span*81n)/1600n},800,600),null);
}
assert.match(a.nearbyReferenceCandidates.toString(),/deepContexts/,'GPU-resident guided refs participate in nearby reuse');
// Worker lifecycle: idle destroy still terminates/revokes resources.
for(const Service of [a.FastReferenceService,a.PrecisionFallbackService]){
const s=new Service();let terminated=0;s.worker={terminate(){terminated++}};s.url='blob:idle';s.destroy();assert.equal(terminated,1);assert.equal(s.worker,null);assert.equal(s.url,'');
}
// Correction lazy compile contains only the five actually used pipelines.
{
const src=a.WebGpuRenderer.prototype.ensureCorrectionPipelines.toString();
for(const name of ['DEEP_BUCKET_HIST_WGSL','DEEP_BUCKET_PREFIX_WGSL','DEEP_BUCKET_SCATTER_WGSL','DEEP_CORRECT_WGSL','DEEP_CORRECT_QUEUE_WGSL'])assert.match(src,new RegExp(name));
assert.doesNotMatch(src,/DEEP_PERTURB/);assert.equal((src.match(/this\.module\(/g)||[]).length,5);
}
// Render cancellation must cancel export and advance the generation token.
{
Object.assign(a.state,{token:40,rendering:true,recolorPending:true,deferNumericPublish:true,renderClock:null});a.exportJob.active=true;a.exportJob.cancelled=false;
a.cancelRender();assert.equal(a.exportJob.cancelled,true);assert.equal(a.state.token,41);assert.equal(a.state.rendering,false);
a.exportJob.active=false;
}
// Both export paths share one metadata schema builder.
{
Object.assign(a.state,{adaptive:true,baseIter:350,unknownReasons:{operationLimit:7},fieldView:{backend:'pixel-frontier-fast-extended',membershipCertified:false,frontier:{finiteBudgetComplete:true,policy:'explicit-gpu-resident-finite-budget'}}});
const snap={bits:256,re:1n,im:2n,span:3n},palette={id:0,cycle:.008,shift:.18,edgeAA:false};
const completed=a.buildExportMetadata({snap,iter:4096,w:800,h:600,palette,completedField:true,exportPipeline:'completed-numeric-field'});
const rerender=a.buildExportMetadata({snap,iter:4096,w:1600,h:1200,ss:2,palette:{...palette,edgeAA:true},fastExtended:true,strict:true,unresolvedSamples:3,exportPipeline:'tiles',exportTileSize:128,exportRingDepth:1});
for(const key of ['format','rendererVersion','backend','numericEngine','finiteBudgetComplete','membershipCertified','precisionPolicy','pixelContract','width','height','supersampling','numericSamples','unresolvedSamples','iterationPolicy','view','palette','exportPipeline','shaderVersion']){assert.ok(key in completed,key);assert.ok(key in rerender,key)}
assert.equal(completed.operationLimit,7);assert.equal(rerender.exportTileSize,128);assert.equal(rerender.exportRingDepth,1);assert.equal(rerender.membershipCertified,false);
}
// Run surviving CPU workers at tiny budgets.
function worker(source,message){let result;const wc={self:{},postMessage:r=>{result=r},performance};vm.runInNewContext(source,wc);wc.self.onmessage({data:message});assert.ok(result);assert.notEqual(result.type,'error',result.error);return result}
{
const scale=1n<<256n,message={type:'build',id:1,key:'tiny',bits:256,sourceBits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,iter:64};
const guarded=worker(a.referenceWorkerSource(),message),fast=worker(a.fastReferenceWorkerSource(),message);
assert.equal(guarded.checkpointMismatch,false);assert.equal(guarded.refLen,64);assert.equal(fast.refLen,64);assert.deepEqual(Buffer.from(guarded.refs),Buffer.from(fast.refs));
assert.equal(fast.series.jump,0);assert.equal(fast.series.coeffs.length,8);
const escaped=worker(a.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:256,targetBits:128,re:(3n*scale).toString(),im:'0',span:'0',width:1,height:1,iter:64,terminalClass:-1,indices:Uint32Array.of(0).buffer});
assert.equal(new Uint8Array(escaped.accepted)[0],1);assert.equal(new Uint32Array(escaped.meta)[0]>>>28,1);
const cpu=worker(a.cpuFallbackWorkerSource(),{type:'render',id:3,w:4,h:4,y0:0,y1:4,maxIter:64,re:-.5,im:0,span:3.4,style:{palette:0,cycle:1,shift:0}});
assert.equal(cpu.type,'done');assert.equal(new Uint8ClampedArray(cpu.rgba).length,4*4*4);assert.ok(cpu.work>0);
}
assert.match(a.recoverNumericalGuided.toString(),/FAILURE_GUIDED_REF_MAX_PASSES/);
console.log('PASS: single-source finite viewer regression (production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata)');