Files
Endoleap/tests/headless/copy-snapshot-index.test.mjs
T

36 lines
6.6 KiB
JavaScript

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {createDocument,createRegistry,builtinComponents,planDuplicate,validateDocument} from '../../src/lib/canvas-core/index.ts';
import {encodeIndexedCopySnapshot,openIndexedCopySnapshot} from '../../src/lib/canvas-runtime/copy-snapshot-index.ts';
import {prepareDuplicateStream} from '../../src/lib/canvas-runtime/duplicate.ts';
import {copyRegistrySignature,decodeCopySnapshot} from '../../src/lib/canvas-runtime/copy-snapshot.ts';
import {createDuplicatePlanSource} from '../../src/lib/canvas-runtime/duplicate-source.ts';
const registry=createRegistry(builtinComponents()),signal=()=>new AbortController().signal,node=(id,owner='P',componentId='text')=>({id,incarnation:2,structure:'node',ownerCanvasId:owner,componentId,dataVersion:1,data:{title:id,text:'unparsed-payload-秘密',extra:{retained:true}},geometry:{x:10,y:20,width:200,height:100,rotation:0}});
const document=createDocument('project',[{id:'P',parent:null},{id:'S',parent:{canvasId:'P',entryElementId:'F'}}],[node('C'),{...node('F','P','frame'),data:{...node('F').data,ports:[{id:'port',nodeId:'A'}],presentation:{unitScale:2,contentScale:.75,other:'not-needed-in-index'}},geometry:{...node('F').geometry,rotation:35}},node('A','S','image'),{id:'edge',incarnation:1,structure:'connection',ownerCanvasId:'P',componentId:'connection',dataVersion:1,data:{retained:true},source:{elementId:'C'},target:{elementId:'A'},path:{type:'bezier-chain',knots:[{id:'k',mode:'corner',position:{x:70,y:80},in:{x:1,y:2},out:{x:3,y:4}}]}}],new Map([['deleted',4]]));
const lines=async bytes=>(await bytes.text()).trimEnd().split('\n');
test('indexed snapshots read topology before payload, preserve raw records on demand and reproduce exact mapped output',async()=>{
const bytes=await encodeIndexedCopySnapshot(document,signal()),snapshot=await openIndexedCopySnapshot(bytes,'project',registry,signal());assert.equal(JSON.stringify([...snapshot.document.elements]).includes('unparsed-payload'),false);assert.equal(snapshot.document.elements.get('F').data.ports,undefined);assert.deepEqual(snapshot.document.elements.get('F').data.presentation,{unitScale:2,contentScale:.75});assert.deepEqual([...snapshot.document.tombstones],[...document.tombstones]);assert.throws(()=>validateDocument(snapshot.document,registry));
const loaded=[];let next=0;const state=await prepareDuplicateStream(snapshot.document,registry,[{id:'F',incarnation:2}],{createId:()=>`copy-${++next}`},{sourceToken:'a'.repeat(64),schemaToken:copyRegistrySignature(snapshot.document,registry),topologyOnly:true});assert.equal(loaded.length,0);const source=createDuplicatePlanSource(snapshot.document,registry,state,'indexed',async(id,abort)=>{loaded.push(id);return snapshot.readElement(id,abort);});let reference=0;const expected=planDuplicate(document,registry,[{id:'F',incarnation:2}],{createId:()=>`copy-${++reference}`});
const last=await source.read(expected.changes.length-1,signal());assert.deepEqual(last.value.change,expected.changes.at(-1));assert.deepEqual(loaded,['edge'],'edge geometry decoded unrelated node payload');
for(let i=0;i<expected.changes.length;i++)assert.deepEqual((await source.read(i,signal())).value.change,expected.changes[i]);for(const[id,record]of document.elements)assert.deepEqual(await snapshot.readElement(id,signal()),record);await assert.rejects(decodeCopySnapshot(bytes,'project',registry,signal()),/invalid_copy_snapshot/);
});
test('index decoding defers corrupt or invalid component content until the corresponding record is requested',async()=>{
const rows=await lines(await encodeIndexedCopySnapshot(document,signal())),last=rows.length-1;rows[last]='!'+rows[last].slice(1);const corrupt=new Blob([rows.join('\n')+'\n']),snapshot=await openIndexedCopySnapshot(corrupt,'project',registry,signal());assert.equal((await snapshot.readElement('A',signal())).id,'A');await assert.rejects(snapshot.readElement('edge',signal()),/invalid_copy_snapshot/);
const clean=await lines(await encodeIndexedCopySnapshot(document,signal())),i=clean.findIndex(row=>row.startsWith('{"kind":"element","record":{"id":"A"'));clean[i]=clean[i].replace('"id":"A"','"id":"Z"');const mismatch=await openIndexedCopySnapshot(new Blob([clean.join('\n')+'\n']),'project',registry,signal());await assert.rejects(mismatch.readElement('A',signal()),/invalid_copy_snapshot/);
});
test('indexed source rejects locator gaps, geometry corruption, truncation, foreign versions and aborted reads',async()=>{
const bytes=await encodeIndexedCopySnapshot(document,signal()),rows=await lines(bytes),index=rows.findIndex(row=>JSON.parse(row).kind==='element-index');
for(const change of [row=>({...row,offset:1}),row=>({...row,byteLength:0}),row=>({...row,record:{...row.record,geometry:{...row.record.geometry,width:0}}})]){const bad=[...rows];bad[index]=JSON.stringify(change(JSON.parse(bad[index])));await assert.rejects(openIndexedCopySnapshot(new Blob([bad.join('\n')+'\n']),'project',registry,signal()));}
await assert.rejects(openIndexedCopySnapshot(bytes.slice(0,bytes.size-1),'project',registry,signal()));await assert.rejects(openIndexedCopySnapshot(new Blob([await bytes.text(),'x']),'project',registry,signal()));await assert.rejects(openIndexedCopySnapshot(bytes,'foreign',registry,signal()));const controller=new AbortController();controller.abort();await assert.rejects(encodeIndexedCopySnapshot(document,controller.signal),/copy_aborted/);await assert.rejects(openIndexedCopySnapshot(bytes,'project',registry,controller.signal),/copy_aborted/);const snapshot=await openIndexedCopySnapshot(bytes,'project',registry,signal());await assert.rejects(snapshot.readElement('A',controller.signal),/copy_aborted/);
});
test('bounded raw windows preserve sequential parser batching and evict old bytes without caching decoded content',async()=>{
const source=createDocument('project',[{id:'P',parent:null}],Array.from({length:120},(_,i)=>({...node(`record-${i}`),data:{title:`record-${i}`,text:'x'.repeat(3500)}}))),bytes=await encodeIndexedCopySnapshot(source,signal()),reads=[];
class ObservedBlob extends Blob{slice(...args){reads.push(args);return super.slice(...args);}}
const snapshot=await openIndexedCopySnapshot(new ObservedBlob([bytes]),'project',registry,signal());reads.length=0;
for(let i=0;i<120;i++)assert.equal((await snapshot.readElement(`record-${i}`,signal())).data.title,`record-${i}`);assert.ok(reads.length>=5&&reads.length<120/4,`unexpected per-record Blob reads: ${reads.length}`);const count=reads.length;assert.equal((await snapshot.readElement('record-0',signal())).data.text.length,3500);assert.equal(reads.length,count+1,'first window did not leave the bounded cache');
});