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); }); 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')); });