66 lines
4.8 KiB
JavaScript
66 lines
4.8 KiB
JavaScript
import test from 'node:test';
|
|
import {readFileSync} from 'node:fs';
|
|
import assert from 'node:assert/strict';
|
|
import {parseCanvasDocument,deriveNode,connectNodes,removeCanvasNode,referencedNodes,agentTask} from '../src/lib/canvas-document.ts';
|
|
const note={id:'brief',kind:'text',title:'创意简报',text:'清晨的城市',x:20,y:50};
|
|
const doc={version:1,cards:[note],links:[]};
|
|
test('existing v1 browser drafts remain readable and untrusted fields are discarded',()=>{
|
|
assert.deepEqual(parseCanvasDocument(JSON.stringify({...doc,token:'ignored',cards:[{...note,html:'ignored'}]})),doc);
|
|
for(const bad of [{...doc,cards:[note,note]},{...doc,links:[['brief','missing']]},{...doc,cards:[{...note,image:'https://example.com/x.png'}]},{...doc,cards:[{...note,style:{color:'url(javascript:bad)'}}]}])assert.throws(()=>parseCanvasDocument(bad));
|
|
});
|
|
test('derived image and script nodes preserve directed references across save and restore',()=>{
|
|
const picture={...note,id:'picture',kind:'image',title:'参考图',image:'data:image/png;base64,AAAA',operation:'generate'};
|
|
const next=deriveNode(doc,'brief',picture);
|
|
const restored=parseCanvasDocument(JSON.stringify(deriveNode(next,'picture',{...note,id:'script',kind:'script',operation:'script'})));
|
|
assert.equal(referencedNodes(restored,'picture')[0].id,'brief');assert.equal(referencedNodes(restored,'script')[0].id,'picture');
|
|
assert.throws(()=>connectNodes(restored,'script','brief'),/循环/);
|
|
assert.throws(()=>parseCanvasDocument({...restored,links:[...restored.links,['script','brief']]}),/循环/);
|
|
const removed=removeCanvasNode(restored,'picture');assert.deepEqual(removed.links,[]);assert.equal(removed.cards.length,2);
|
|
});
|
|
test('agent task captures current referenced content and validates upscale inputs',()=>{
|
|
const next=deriveNode(doc,'brief',{...note,id:'out',kind:'script',operation:'script'});
|
|
assert.equal(agentTask(next,'out').references[0].text,'清晨的城市');
|
|
assert.equal(agentTask(next,'out').result.targetId,'out');
|
|
assert.throws(()=>agentTask({...next,cards:next.cards.map(n=>n.id==='out'?{...n,operation:'upscale'}:n)},'out'),/引用一张图片/);
|
|
});
|
|
|
|
test('instructions stay distinct from generated content in agent requests',()=>{
|
|
const result={...note,id:'script',kind:'script',text:'已有脚本正文',instruction:'改写为六个分镜',operation:'script'};
|
|
const restored=parseCanvasDocument(JSON.stringify(deriveNode(doc,'brief',result)));
|
|
assert.equal(agentTask(restored,'script').instruction,'改写为六个分镜');
|
|
assert.equal(restored.cards.find(n=>n.id==='script').text,'已有脚本正文');
|
|
});
|
|
|
|
test('editing an image passes the current image to the external agent',()=>{
|
|
const image={...note,id:'image',kind:'image',instruction:'把背景改为雪夜',image:'data:image/png;base64,AAAA'};
|
|
assert.equal(agentTask({version:1,cards:[image],links:[]},'image').sourceImage,image.image);
|
|
});
|
|
|
|
const overlaps=(a,b)=>a.x<b.x+(b.width||320)&&a.x+(a.width||320)>b.x&&a.y<b.y+(b.height||350)&&a.y+(a.height||350)>b.y;
|
|
test('branching the example keeps the source image accessible after image and script derivation',()=>{
|
|
const scene={version:1,cards:[
|
|
{...note,x:70,y:140},
|
|
{...note,id:'reference',kind:'image',x:460,y:80},
|
|
{...note,id:'shot',kind:'prompt',x:850,y:160},
|
|
],links:[['brief','reference'],['reference','shot']]};
|
|
const image=deriveNode(scene,'reference',{...note,id:'hd',kind:'image',operation:'upscale',x:930,y:80,width:360,height:300});
|
|
const result=deriveNode(image,'brief',{...note,id:'script',kind:'script',operation:'script',x:540,y:140,width:360,height:300});
|
|
assert.deepEqual(result.cards.slice(0,3),scene.cards,'Existing content and positions stay unchanged');
|
|
for(const added of result.cards.slice(3))for(const other of result.cards.filter(n=>n.id!==added.id))assert.equal(overlaps(added,other),false,`${added.id} blocks ${other.id}`);
|
|
assert.equal(referencedNodes(result,'hd')[0].id,'reference');
|
|
assert.equal(referencedNodes(result,'script')[0].id,'brief');
|
|
assert.deepEqual(parseCanvasDocument(JSON.stringify(result)),result);
|
|
});
|
|
test('a free requested position is retained and large nodes near the coordinate limit can still branch',()=>{
|
|
const free=deriveNode(doc,'brief',{...note,id:'free',x:2000,y:50});
|
|
assert.deepEqual({x:free.cards[1].x,y:free.cards[1].y},{x:2000,y:50});
|
|
const edge={version:1,cards:[{...note,x:90000,y:99600,width:1200,height:1200}],links:[]};
|
|
const result=deriveNode(edge,'brief',{...note,id:'edge',x:90500,y:99700,width:360,height:300});
|
|
assert.equal(overlaps(result.cards[0],result.cards[1]),false);
|
|
assert.deepEqual(parseCanvasDocument(JSON.stringify(result)),result);
|
|
});
|
|
|
|
test('agent and browser parse the same canvas document contract',()=>{
|
|
assert.equal(readFileSync(new URL('../src/lib/canvas-document.ts',import.meta.url),'utf8'),readFileSync(new URL('../../waterfall-wiki/src/lib/canvas-document.ts',import.meta.url),'utf8'));
|
|
});
|