import assert from 'node:assert/strict';import {test} from 'node:test';import {loadTypescript} from './support/load-typescript.mjs'; const tick=async()=>{for(let i=0;i<8;i++)await Promise.resolve();}; async function harness(){const workers=[],bitmaps=[];const oldW=globalThis.Worker,oldB=globalThis.createImageBitmap;globalThis.Worker=class{constructor(){this.messages=[];this.terminated=false;workers.push(this);}postMessage(m){this.messages.push(m);}terminate(){this.terminated=true;}};globalThis.createImageBitmap=async()=>{const b={width:10,height:10,closed:false,close(){this.closed=true;}};bitmaps.push(b);return b;};const {CutoutProvider}=await loadTypescript(new URL('../src/cutout/CutoutProvider.ts',import.meta.url));const statuses=[];const p=new CutoutProvider((id,m)=>statuses.push([id,m]));return{p,workers,bitmaps,statuses,cleanup(){p.cancel();globalThis.Worker=oldW;globalThis.createImageBitmap=oldB;}};} const snapshot={requestId:12,width:10,height:10,bitmap:{}}; function success(w){const alpha=new Uint8Array(100);alpha.fill(255,30,60);w.onmessage({data:{type:'result',id:12,width:10,height:10,alpha:alpha.buffer,run:{model:'u2netp',revision:'test',backend:'wasm',downloadBytes:0,preparationMs:1,inferenceMs:1,totalMs:2}}});} test('provider validates result and terminates inference worker before resolving',async()=>{const h=await harness();try{const task=h.p.run(snapshot,new AbortController().signal);await tick();success(h.workers[0]);const result=await task;assert.equal(result.requestId,12);assert.equal(h.workers[0].terminated,true);assert.ok(result.bounds.x1>result.bounds.x0);}finally{h.cleanup();}}); test('GPU failure retries at most once in a fresh WASM worker; stale messages ignored',async()=>{const h=await harness();try{const task=h.p.run(snapshot,new AbortController().signal);await tick();const old=h.workers[0];old.onmessage({data:{type:'fallback',id:12}});await tick();assert.equal(old.terminated,true);assert.equal(h.workers[1].messages[0].forceWasm,true);old.onmessage({data:{type:'error',id:12,message:'late'}});success(h.workers[1]);await task;assert.equal(h.workers.length,2);}finally{h.cleanup();}}); test('abort terminates and rejects, never accepting a late result or status',async()=>{const h=await harness();try{const abort=new AbortController();const task=h.p.run(snapshot,abort.signal);const rejected=assert.rejects(task,{name:'AbortError'});await tick();const w=h.workers[0];abort.abort();await rejected;success(w);w.onmessage({data:{type:'status',id:12,message:'late'}});assert.equal(w.terminated,true);assert.equal(h.statuses.length,0);}finally{h.cleanup();}}); test('photo snapshot applies EXIF once then constrains canonical longest edge',async()=>{const original=globalThis.createImageBitmap;const calls=[];let closed=0;globalThis.createImageBitmap=async(source,options)=>{calls.push(options);return calls.length===1?{width:1600,height:800,close(){closed++;}}:{width:1024,height:512,close(){closed++;}};};try{const {createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const result=await createPhotoSnapshot({},2,new AbortController().signal);assert.equal(result.width,1024);assert.equal(result.height,512);assert.equal(calls[0].imageOrientation,'from-image');assert.equal(calls[1].resizeWidth,1024);assert.equal(closed,1);}finally{globalThis.createImageBitmap=original;}}); test('late rejected capture never rejects a newer request',async()=>{const h=await harness();try{let rejectCapture;globalThis.createImageBitmap=()=>new Promise((_,reject)=>{rejectCapture=reject;});const first=h.p.run(snapshot,new AbortController().signal);const firstRejected=assert.rejects(first,{name:'AbortError'});h.p.cancel();await firstRejected;globalThis.createImageBitmap=async()=>({width:10,height:10,close(){}});const next=h.p.run(snapshot,new AbortController().signal);const settled=next.then(value=>({value}),error=>({error}));await tick();rejectCapture(new Error('old decode failure'));await tick();success(h.workers[0]);assert.equal((await settled).error,undefined);}finally{h.cleanup();}}); test('worker construction failure closes the untransferred clone',async()=>{const h=await harness();try{globalThis.Worker=class{constructor(){throw new Error('worker unavailable');}};await assert.rejects(h.p.run(snapshot,new AbortController().signal),/worker unavailable/);assert.equal(h.bitmaps[0].closed,true);}finally{h.cleanup();}}); test('abort while clone is pending closes only clone and never creates a worker',async()=>{const h=await harness();try{let resolveCapture;let canonicalClosed=0,cloneClosed=0;const snap={...snapshot,bitmap:{close(){canonicalClosed++;}}};globalThis.createImageBitmap=()=>new Promise(resolve=>{resolveCapture=resolve;});const abort=new AbortController(),task=h.p.run(snap,abort.signal);const rejected=assert.rejects(task,{name:'AbortError'});abort.abort();await rejected;resolveCapture({width:10,height:10,close(){cloneClosed++;}});await tick();assert.equal(cloneClosed,1);assert.equal(canonicalClosed,0);assert.equal(h.workers.length,0);}finally{h.cleanup();}}); test('second fallback fails with no third worker',async()=>{const h=await harness();try{const task=h.p.run(snapshot,new AbortController().signal);const rejected=assert.rejects(task,/계산하지 못/);await tick();h.workers[0].onmessage({data:{type:'fallback',id:12}});await tick();h.workers[1].onmessage({data:{type:'fallback',id:12}});await rejected;assert.equal(h.workers.length,2);assert.ok(h.workers.every(w=>w.terminated));}finally{h.cleanup();}}); test('snapshot abort during resize closes decoded and resized bitmaps exactly once',async()=>{const old=globalThis.createImageBitmap;let resolveResize,decodedClosed=0,resizedClosed=0;let count=0;globalThis.createImageBitmap=async()=>{if(++count===1)return{width:1600,height:800,close(){decodedClosed++;}};return new Promise(resolve=>{resolveResize=resolve;});};try{const {createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const abort=new AbortController(),task=createPhotoSnapshot({},2,abort.signal),rejected=assert.rejects(task,{name:'AbortError'});await tick();abort.abort();resolveResize({width:1024,height:512,close(){resizedClosed++;}});await rejected;assert.equal(decodedClosed,1);assert.equal(resizedClosed,1);}finally{globalThis.createImageBitmap=old;}}); test('disposing a provider cancels active work and permanently prevents a new worker',async()=>{const h=await harness();try{const task=h.p.run(snapshot,new AbortController().signal),rejected=assert.rejects(task,{name:'AbortError'});await tick();h.p.dispose();h.p.dispose();await rejected;assert.equal(h.workers[0].terminated,true);await assert.rejects(h.p.run(snapshot,new AbortController().signal),/종료/);assert.equal(h.workers.length,1);}finally{h.cleanup();}});