import test from 'node:test';import assert from 'node:assert/strict';import {AssetQueue,fetchSceneBytes} from '../assets/scene-assets.js'; const tick=()=>new Promise(r=>setTimeout(r,0)); test('shared requests deduplicate, concurrency is capped, and releasing the last viewer cancels stale work',async()=>{ let started=[],resolve={},aborted=[],disposed=[]; const q=new AssetQueue({maxConcurrent:2,maxIdle:0,dispose:a=>disposed.push(a.key),load:(key,signal)=>new Promise((r,j)=>{started.push(key);resolve[key]=()=>r({key,bytes:1});signal.addEventListener('abort',()=>{aborted.push(key);j(new DOMException('Aborted','AbortError'));});})}); const a=q.retain('A',()=>{}),a2=q.retain('A',()=>{}),b=q.retain('B',()=>{}),c=q.retain('C',()=>{});await tick();assert.deepEqual(started,['A','B']);assert.equal(q.peakRunning,2); a.release();assert.equal(aborted.length,0);a2.release();await tick();assert.deepEqual(aborted,['A']);assert.deepEqual(started,['A','B','C']); resolve.B();resolve.C();await tick();b.release();c.release();assert.equal(q.running,0);assert.equal(q.entries.size,0);assert.deepEqual(disposed.sort(),['B','C']); }); test('a transient error retries and a later subscriber reuses the asset',async()=>{ let attempts=0;const states=[];const q=new AssetQueue({load:async key=>{if(++attempts===1)throw Error('503');return {key,bytes:2};},dispose:()=>{}}); const a=q.retain('A',e=>states.push(e.state));await new Promise(r=>setTimeout(r,850));assert.equal(attempts,2);assert.ok(states.includes('retrying'));assert.equal(states.at(-1),'ready'); const b=q.retain('A',()=>{});await tick();assert.equal(attempts,2);a.release();b.release(); }); test('a stalled fetch reaches a bounded error, and partial responses are rejected',async()=>{ const stalled=(_url,{signal})=>new Promise((resolve,reject)=>signal.addEventListener('abort',()=>reject(new DOMException('Abort','AbortError')))); await assert.rejects(fetchSceneBytes('test',new AbortController().signal,()=>{},{idleMs:15,totalMs:100,fetcher:stalled}),/stopped responding/); await assert.rejects(fetchSceneBytes('test',new AbortController().signal,()=>{},{fetcher:async()=>new Response(new Uint8Array([1,2]),{headers:{'content-length':'4'}})}),/incomplete/); }); test('download progress retains every byte and aborts propagate',async()=>{ const p=[];const b=await fetchSceneBytes('test',new AbortController().signal,x=>p.push(x),{fetcher:async()=>new Response(new Uint8Array([1,2,3]),{headers:{'content-length':'3'}})});assert.deepEqual([...b],[1,2,3]);assert.equal(p.at(-1).received,3); const c=new AbortController();c.abort();await assert.rejects(fetchSceneBytes('test',c.signal,()=>{},{fetcher:async(_,{signal})=>{signal.throwIfAborted();}}),{name:'AbortError'}); });