'use strict'; const path=require('path'); const vm=require('vm'); const {assert,root,read}=require('./helpers/app-source'); delete global.BendFieldPersistence; require(path.join(root,'field-persistence.js')); const persistence=global.BendFieldPersistence; assert(persistence&&persistence.ARCHIVE_VERSION===2,'Archive persistence module did not initialize'); const manifest={ saveSchema:31,gameplayVersion:3,worldGeneration:'test-world',appVersion:'47.36',generatorVersion:1, exportedAt:'2026-07-28T00:00:00.000Z',boardCount:2,estimatedRawBytes:4096,encoding:'ndjson',compression:'identity' }; const globalState={gameplayVersion:3,bonusEvents:{},fieldEffects:[],specialMechanicsSeen:[],clockFloor:0,lastSolveAt:0,timeAttack:null,timeAttackCooldowns:{3:0,5:0,10:0},lastTimeAttack:null}; const boardRows=[ {id:'B0',meta:{id:'B0',x:0,y:0,chunks:[[0,0]],puzzle:{g:[[0,0,0],[0,1,2]],n:[],valid:[[0,0],[0,1]],solution:[]}},state:{paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,scoreAwarded:0}}, {id:'B1',meta:{id:'B1',x:1,y:0,chunks:[[0,0]],puzzle:{g:[[0,0,0],[0,1,2]],n:[],valid:[[0,0],[0,1]],solution:[]}},state:{paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,scoreAwarded:0}} ]; async function* boards(){for(const row of boardRows)yield structuredClone(row)} async function encode(compression,module=persistence){ const chunks=[]; const writable=new WritableStream({write(chunk){chunks.push((chunk instanceof Uint8Array?chunk:new Uint8Array(chunk)).slice())}}); const result=await module.writeArchive({writable,manifest:{...manifest,compression},globalState:structuredClone(globalState),boards:boards()}); return{result,blob:new Blob(chunks,{type:'application/x-bend-field-save'}),bytes:Buffer.concat(chunks.map(chunk=>Buffer.from(chunk)))}; } async function decode(blob,module=persistence){ const seen=[]; const result=await module.readArchive(blob,{onBoard:record=>seen.push(record)}); return{result,seen}; } (async()=>{ for(const compression of ['identity','gzip']){ if(compression==='gzip'&&typeof CompressionStream!=='function')continue; const first=await encode(compression),second=await encode(compression),decoded=await decode(first.blob); assert(first.result.boardCount===2&&decoded.result.boardCount===2&&decoded.seen.map(row=>row.id).join(',')==='B0,B1',`${compression} archive did not round-trip`); assert(first.result.crc32===decoded.result.footer.crc32,`${compression} archive checksum changed during round-trip`); assert(first.bytes.equals(second.bytes),`${compression} archive output is not deterministic for identical records`); } const identity=await encode('identity'),text=identity.bytes.toString('utf8'),modified=text.replace('"x":1','"x":2'); assert(modified!==text,'Archive corruption fixture did not modify a board record'); let checksumRejected=false; try{await decode(new Blob([modified]))}catch(error){checksumRejected=error?.code==='CHECKSUM_MISMATCH'} assert(checksumRejected,'Archive checksum corruption was not rejected'); let truncationRejected=false; try{await decode(new Blob([text.split('\n').slice(0,-2).join('\n')+'\n']))}catch(error){truncationRejected=/footer|incomplete/i.test(error?.message||'')} assert(truncationRejected,'Truncated archive was not rejected'); let manifestRejected=false; try{await persistence.inspectArchive(new Blob([text.replace('\"encoding\":\"ndjson\"','\"encoding\":\"json\"')]))}catch(error){manifestRejected=error?.code==='UNSUPPORTED_ARCHIVE'} assert(manifestRejected,'Unsupported archive encoding was not rejected at inspection time'); const lines=text.trimEnd().split('\n'); const expectArchiveFailure=async(candidate,pattern,label)=>{let rejected=false;try{await decode(new Blob([candidate]))}catch(error){rejected=pattern.test(`${error?.code||''} ${error?.message||''}`)}assert(rejected,label)}; const reordered=[...lines];[reordered[2],reordered[3]]=[reordered[3],reordered[2]];await expectArchiveFailure(`${reordered.join('\n')}\n`,/order|B0/i,'Reordered archive boards were accepted'); const duplicate=[...lines];duplicate[3]=duplicate[2];await expectArchiveFailure(`${duplicate.join('\n')}\n`,/duplicated|order/i,'Duplicate archive board IDs were accepted'); const unknown=[...lines],unknownRecord=JSON.parse(unknown[3]);unknownRecord.type='mystery';unknown[3]=JSON.stringify(unknownRecord);await expectArchiveFailure(`${unknown.join('\n')}\n`,/unknown|misplaced/i,'Unknown archive records were accepted'); const wrongFooter=[...lines],footer=JSON.parse(wrongFooter[4]);footer.rawBytes++;wrongFooter[4]=JSON.stringify(footer);await expectArchiveFailure(`${wrongFooter.join('\n')}\n`,/CHECKSUM|integrity/i,'Incorrect footer byte counts were accepted'); const wrongCount=[...lines],declared=JSON.parse(wrongCount[0]);declared.boardCount=1;wrongCount[0]=JSON.stringify(declared);await expectArchiveFailure(`${wrongCount.join('\n')}\n`,/more boards|count/i,'Incorrect manifest board counts were accepted'); const aborted=new AbortController();aborted.abort();let canceled=false;try{await persistence.readArchive(identity.blob,{signal:aborted.signal})}catch(error){canceled=error?.name==='AbortError'}assert(canceled,'Archive cancellation was not acknowledged'); let writeFailed=false;try{await persistence.writeArchive({writable:new WritableStream({write(){throw new Error('injected destination failure')}}),manifest:{...manifest},globalState:structuredClone(globalState),boards:boards()})}catch(_){writeFailed=true}assert(writeFailed,'Destination write failure did not abort export'); if(typeof CompressionStream==='function'&&typeof DecompressionStream==='function'){const compressed=await encode('gzip'),damaged=Buffer.from(compressed.bytes);damaged[Math.max(10,Math.floor(damaged.length/2))]^=0xff;let gzipRejected=false;try{await decode(new Blob([damaged]))}catch(_){gzipRejected=true}assert(gzipRejected,'Corrupt gzip archive was accepted')} const worker=read('field-persistence-worker.js'),moduleSource=read('field-persistence.js'); class FakeWorker{ constructor(){this.onmessage=null;this.onerror=null;const owner=this,archiveCodec=require('../archive-codec'),context={TextEncoder,Uint8Array,Uint32Array,JSON,Error,importScripts:()=>{},self:{BendArchiveCodec:archiveCodec,postMessage(data){queueMicrotask(()=>owner.onmessage?.({data}))}}};vm.createContext(context);vm.runInContext(worker,context);this.workerSelf=context.self} postMessage(data){queueMicrotask(()=>{try{this.workerSelf.onmessage({data:structuredClone(data)})}catch(error){this.onerror?.({message:error.message})}})} terminate(){} } global.Worker=FakeWorker;delete global.BendFieldPersistence;delete require.cache[require.resolve(path.join(root,'field-persistence.js'))];require(path.join(root,'field-persistence.js'));const workerPersistence=global.BendFieldPersistence; const workerEncoded=await encode('identity',workerPersistence),workerDecoded=await decode(workerEncoded.blob,workerPersistence); assert(workerEncoded.result.worker===true&&workerDecoded.result.worker===true&&workerDecoded.seen.length===2,'Archive worker execution path did not round-trip records'); delete global.Worker;global.BendFieldPersistence=persistence; assert(worker.includes("importScripts('archive-codec.js')")&&worker.includes('MAX_CHUNK_BYTES=1024*1024')&&worker.includes('postMessage({id,chunks,rawBytes,crc32:crc32Hex(crc)},chunks)'),'Archive worker does not share the codec or enforce transferable 1 MiB output chunks'); assert(moduleSource.includes("new Worker('field-persistence-worker.js')")&&moduleSource.includes('item.byteLength>MAX_ARCHIVE_LINE_BYTES')&&moduleSource.includes('WORKER_TARGET_BYTES=1024*1024')&&moduleSource.includes('pendingLineBytes'),'Archive worker path, 1 MiB batching, or worker line limit is missing'); console.log('Field archive round-trip and corruption checks passed'); })().catch(error=>{console.error(error);process.exitCode=1});