import test from 'node:test'; import assert from 'node:assert/strict'; import { fetchCanvasResponse } from '../src/lib/canvas-response.ts'; import { api, ApiError } from '../src/lib/api.ts'; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); test('snapshot body outlives the header deadline and reports transfer timing and bytes',async()=>{ const reports=[]; const bytes=new TextEncoder().encode('{"version":210,"document":{"title":"测试"}}'); const upstream=new Response(new ReadableStream({async start(c){ c.enqueue(bytes.slice(0,10));await wait(50);c.enqueue(bytes.slice(10));c.close();}}),{headers:{ETag:'"210"'}}); const response=await fetchCanvasResponse('https://example.test',{}, {headersMs:10,bodyMs:500,idleMs:300},async()=>upstream, report=>reports.push(report)); assert.equal(response.headers.get('etag'),'"210"');assert.equal((await response.json()).version,210); assert.equal(reports.length,1);assert.equal(reports[0].outcome,'complete');assert.equal(reports[0].bytes,bytes.length);assert.ok(reports[0].transfer_ms>=30);assert.equal(reports[0].chunks,2); }); test('snapshot idle timeout aborts unfinished bodies instead of returning a valid empty snapshot',async()=>{ const reports=[];let cancelled=false; const upstream=new Response(new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('{"version":'));},cancel(){cancelled=true;}})); const response=await fetchCanvasResponse('https://example.test',{}, {headersMs:100,bodyMs:500,idleMs:15},async()=>upstream,r=>reports.push(r)); await assert.rejects(response.json(),{name:'TimeoutError'});assert.ok(cancelled);assert.equal(reports[0].outcome,'error');assert.equal(reports[0].error,'TimeoutError'); }); test('header deadline rejects a server that has not responded',async()=>{ await assert.rejects(fetchCanvasResponse('https://example.test',{}, {headersMs:10,bodyMs:500,idleMs:100},(_url,options)=>new Promise((_resolve,reject)=>options.signal.addEventListener('abort',()=>reject(options.signal.reason)))),{name:'TimeoutError'}); }); test('disconnect and consumer cancellation release the upstream read',async()=>{ const abort=new AbortController();let cancelled=0; const make=()=>new Response(new ReadableStream({cancel(){cancelled++;}})); const response=await fetchCanvasResponse('https://example.test',{signal:abort.signal},{headersMs:100,bodyMs:500,idleMs:100},async()=>make()); abort.abort();await assert.rejects(response.text(),{name:'AbortError'}); const second=await fetchCanvasResponse('https://example.test',{}, {headersMs:100,bodyMs:500,idleMs:100},async()=>make());await second.body.cancel();assert.equal(cancelled,2); }); test('client distinguishes a failed 200 body transfer, malformed JSON, server errors and 204',async()=>{ const original=globalThis.fetch; try { globalThis.fetch=async()=>new Response(new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('{"version":'));c.error(new TypeError('terminated'));}})); await assert.rejects(api('canvas'),error=>error instanceof ApiError && error.message==='response_interrupted'); globalThis.fetch=async()=>new Response('{"version":');await assert.rejects(api('canvas'),/invalid_api_response/); globalThis.fetch=async()=>Response.json({error:'upstream_timeout'},{status:504});await assert.rejects(api('canvas'),/upstream_timeout/); globalThis.fetch=async()=>new Response(null,{status:204});assert.equal(await api('canvas'),null); } finally {globalThis.fetch=original;} });