import {test} from 'node:test'; import assert from 'node:assert/strict'; import {freeze,validJson} from '../../src/lib/canvas-core/values.ts'; import {encodeRecoveryValue,sealRecoveryImage,isSealedRecoveryImage} from '../../src/lib/canvas-runtime/recovery-encoding.ts'; const json=value=>JSON.parse(JSON.stringify(value)); test('recovery encoding preserves JSON value semantics including omitted, sparse, special-key and custom values',()=>{ const value={number:-0,infinite:Infinity,missing:undefined,symbol:Symbol('s'),fn(){},array:[,undefined,()=>{},Symbol('s'),NaN],date:new Date('2026-01-01T00:00:00Z'),boxed:new Number(4),custom:{toJSON(key){return{key,text:'custom'};}},['__proto__']:{safe:true}}; const encoded=encodeRecoveryValue(value);assert.deepEqual(encoded,json(value));assert.equal(validJson(encoded),true);assert.equal(Object.getPrototypeOf(encoded),Object.prototype);assert.equal(Object.hasOwn(encoded,'__proto__'),true);assert.ok(Object.isFrozen(encoded.array)); }); test('only frozen data subtrees are reused and the encoded image is detached and immutable',()=>{ const shared=freeze({items:[{title:'stable',unknown:{retained:true}}]}),left=encodeRecoveryValue({shared}),right=encodeRecoveryValue({shared});assert.equal(left.shared,right.shared);assert.notEqual(left.shared,shared);assert.throws(()=>{left.shared.items[0].title='changed';},TypeError); const mutable={title:'before'},shallow=Object.freeze({mutable});assert.equal(encodeRecoveryValue(shallow).mutable.title,'before');mutable.title='after';assert.equal(encodeRecoveryValue(shallow).mutable.title,'after');assert.equal(encodeRecoveryValue(shallow).mutable===mutable,false); }); test('frozen getters, custom prototypes and internally mutable objects are evaluated again',()=>{ let current='first';const getter=Object.freeze({get title(){return current;}});assert.equal(encodeRecoveryValue(getter).title,'first');current='second';assert.equal(encodeRecoveryValue(getter).title,'second'); let enabled=false;const dynamic={value:'ordinary'};Object.defineProperty(dynamic,'toJSON',{get(){return enabled?()=>({changed:true}):undefined;}});Object.freeze(dynamic);assert.deepEqual(encodeRecoveryValue(dynamic),{value:'ordinary'});enabled=true;assert.deepEqual(encodeRecoveryValue(dynamic),{changed:true}); const date=Object.freeze(new Date('2026-01-01Z'));const parent=Object.freeze({date});assert.deepEqual(encodeRecoveryValue(parent),json(parent));date.setUTCFullYear(2027);assert.deepEqual(encodeRecoveryValue(parent),json(parent)); const prototype=Object.create(Array.prototype);prototype[0]='old';const array=new Array(1);Object.setPrototypeOf(array,prototype);Object.freeze(array);assert.deepEqual(encodeRecoveryValue(array),['old']);prototype[0]='new';assert.deepEqual(encodeRecoveryValue(array),['new']); }); test('cached subtree height cannot bypass the depth bound and omitted object fields do not count',()=>{ const nest=(count,leaf)=>{for(let i=0;iencodeRecoveryValue(nest(64,leaf)),/invalid_recovery_payload/); assert.equal(validJson(encodeRecoveryValue(nest(64,Object.freeze({optional:undefined})))),true);assert.throws(()=>encodeRecoveryValue(nest(64,Object.freeze([undefined]))),/invalid_recovery_payload/); const cycle={};cycle.self=cycle;assert.throws(()=>encodeRecoveryValue(cycle),/invalid_recovery_payload/);assert.throws(()=>encodeRecoveryValue({big:1n})); }); test('only factory-owned images are recognized; immutable-looking impostors retain defensive-copy handling',()=>{ const mutable={title:'before'},image=sealRecoveryImage(2,3,2,{mutable});assert.equal(isSealedRecoveryImage(image),true);assert.equal(isSealedRecoveryImage(structuredClone(image)),false);assert.equal(isSealedRecoveryImage(Object.freeze({...image})),false);mutable.title='after';assert.equal(image.payload.mutable.title,'before');assert.ok(Object.isFrozen(image.payload.mutable));assert.throws(()=>sealRecoveryImage(2,1,2,{}),/corrupt_recovery_image/);assert.equal(isSealedRecoveryImage(sealRecoveryImage(3,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(4,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(5,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(6,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(7,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(8,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(9,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(10,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(11,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(12,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(13,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(14,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(15,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(16,1,1,{})),true);assert.equal(isSealedRecoveryImage(sealRecoveryImage(17,1,1,{})),true);assert.throws(()=>sealRecoveryImage(18,1,1,{}),/corrupt_recovery_image/); }); test('sealed image byte sizes match UTF-8 JSON for reused data and all codec normalization paths',async()=>{ const {sealedRecoveryByteLength}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts'),shared=freeze({text:'δΈ­ζ–‡πŸ˜€\ud800\n"\\',items:[null,1,true]}),payloads=[null,'πŸ˜€',-0,{shared,omitted:undefined,array:[,undefined,NaN]},new Date('2026-01-01Z'),{['__proto__']:'保留'},Object.freeze({get text(){return 'εŠ¨ζ€πŸ˜€';}})]; for(const payload of payloads)for(let revision=0;revision<3;revision++){const image=sealRecoveryImage(17,revision,revision,payload);assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength);assert.equal(sealedRecoveryByteLength(structuredClone(image)),undefined);} }); test('cooperative sealing captures unknown values before yielding and retains exact bytes',async()=>{ const {dataArray,dataRecord}=await import('../../src/lib/canvas-core/data-containers.ts'); const {sealRecoveryImageAsync,sealedRecoveryByteLength}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts'); let current='before',calls=[]; const mutable={title:'before'},shallow=Object.freeze({mutable}),date=Object.freeze(new Date('2026-01-01Z')); const custom=Object.freeze({toJSON(key){calls.push(key);return {key,current};}}); const getter=Object.freeze({get title(){calls.push('getter');return current;}}); const rows=dataArray(Array.from({length:1000},(_,i)=>dataRecord({id:i,text:'δΈ­ζ–‡πŸ˜€\ud800'}))); const payload=dataRecord({rows,shallow,date,getter,left:custom,right:custom,['__proto__']:{safe:true}}); const expected=sealRecoveryImage(17,2,1,payload);calls=[];let yields=0; const pending=sealRecoveryImageAsync(17,2,1,dataRecord({...payload,rows:dataArray(rows)}),{now:(()=>{let t=0;return()=>t++;})(),frameBudgetMs:1,yieldTask:async()=>{yields++;current='after';mutable.title='after';date.setUTCFullYear(2027);}}); assert.deepEqual(calls,['getter','left','right']); mutable.title='outside';current='outside'; const image=await pending; // A second uncached table below exercises the work ceiling with a coarse clock. assert.ok(yields>0);assert.deepEqual(image,expected);assert.equal(isSealedRecoveryImage(image),true); assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength); const beforeCoarse=yields; const large=dataArray(Array.from({length:20000},(_,i)=>i)); const more=await sealRecoveryImageAsync(17,3,2,large,{now:()=>100,frameBudgetMs:1,yieldTask:async()=>{yields++;}}); assert.ok(yields>beforeCoarse);assert.deepEqual(more.payload,large); }); test('cooperative preparation evaluates every unsafe occurrence and rejects errors without an image',async()=>{ const {dataArray,dataRecord}=await import('../../src/lib/canvas-core/data-containers.ts'); const {sealRecoveryImageAsync}=await import('../../src/lib/canvas-runtime/recovery-encoding.ts'); let value=0; const dynamic=Object.freeze({get title(){return ++value;}}),shared=dataRecord({dynamic}); const image=await sealRecoveryImageAsync(17,1,1,dataRecord({left:shared,right:shared})); assert.deepEqual(image.payload,{left:{dynamic:{title:1}},right:{dynamic:{title:2}}}); let ticks=0; await assert.rejects(sealRecoveryImageAsync(17,1,1,dataArray(Array.from({length:1000},(_,i)=>i)),{now:()=>ticks++,frameBudgetMs:1,yieldTask:async()=>{throw Error('yield_failed');}}),/yield_failed/); const nested=(n,leaf)=>{for(let i=0;i