/** * Worker 内部摘要算法选择测试。WebCrypto 可用且未超过单次内存预算时走原生路径,否则使用流式 SHA-256。 * 原生读取/计算失败必须保留原错误,不能默默换算法重试而掩盖资源或执行环境问题。 */ import {test} from 'node:test';import assert from 'node:assert/strict';import {createHash} from 'node:crypto'; import {digestWorkerSource,nativeDigestByteLimit} from '../../src/lib/canvas-runtime/digest-worker-source.ts'; // 空输入、Unicode 和二进制分别比较原生/流式结果,确认预算内原生路径只调用一次正确的 SHA-256 算法。 test('native Worker hashing and streaming fallback produce identical SHA256 for empty, UTF8 and binary sources',async()=>{ for(const bytes of [new Blob(),new Blob(['中文 😀']),new Blob([new Uint8Array([0,255,128,10])])]){ // 注入的 native 只计数并委托 Node WebCrypto;这里执行 Worker 的算法函数,没有启动真实浏览器 Worker。 let calls=0;const native={digest(algorithm,input){calls++;assert.equal(algorithm,'SHA-256');assert.equal(input.byteLength,bytes.size);return crypto.subtle.digest(algorithm,input);}}; assert.equal(await digestWorkerSource(bytes,native),await digestWorkerSource(bytes,null));assert.equal(calls,1); } }); // 整块读取失败、原生 digest 抛错或返回错误长度均明确拒绝;不能改走流式路径生成看似成功的结果。 test('native read or digest failures stay visible and never silently retry through the streaming fallback',async()=>{ // Blob 替身让整块读取失败,同时把 slice 设为陷阱;若代码误走流式兜底会产生另一错误。 class BrokenRead extends Blob{arrayBuffer(){return Promise.reject(Error('read failed'));}slice(){throw Error('unexpected fallback');}} await assert.rejects(digestWorkerSource(new BrokenRead(['input'])),/read failed/); const bytes=new Blob(['input']);await assert.rejects(digestWorkerSource(bytes,{digest(){throw Error('native failed');}}),/native failed/);await assert.rejects(digestWorkerSource(bytes,{async digest(){return new ArrayBuffer(1);}}),/invalid_digest_response/); }); // 超过原生分配预算的输入禁止整块读取及 WebCrypto;仅按最多 1 MiB 块计算,结果与 Node 独立摘要一致。 test('inputs above the native allocation budget use bounded chunks and never request a whole buffer',async()=>{ // 构造刚超过原生预算的逻辑输入,不实际申请整块大数组;Node createHash 独立计算相同零字节序列。 const size=nativeDigestByteLimit+1,hash=createHash('sha256');let largest=0; // 伪 Blob 报告大尺寸,禁止整块 arrayBuffer,只允许 slice 返回有限大小的零字节块并记录峰值。 class ChunkOnly extends Blob{get size(){return size;}arrayBuffer(){throw Error('unbounded read');}slice(start=0,end=size){const count=Math.max(0,Math.min(size,end)-start);largest=Math.max(largest,count);return new Blob([new Uint8Array(count)]);}} for(let cursor=0;cursor