diff --git a/tests/headless/resources.test.mjs b/tests/headless/resources.test.mjs index 3d81bb7..6338c09 100644 --- a/tests/headless/resources.test.mjs +++ b/tests/headless/resources.test.mjs @@ -62,6 +62,7 @@ function fixture() { // 简化请求把完整意图放入 payload,并读取参与 canvas 的 ACK 版本,不进行真实 wire 压缩。 compile(intent, id, snapshot) { return { payload: { intent, id }, canvasIds: intent.canvasIds, expectedVersions: Object.fromEntries(intent.canvasIds.map(id => [id, snapshot.runtime.versions.get(id)])) }; }, // 文档写入应用 changes,递增服务端版本并按 requestId 保存 canonical 回执,供后续接纳恢复使用。 + // 此替身每次 submit 都应用变化,并未实现同 ID 去重或版本拒绝;重开测试用写入次数检查客户端是否重发。 async submit(request) { const { intent } = JSON.parse(request.body); writes.push(request); server = applyChanges(server, intent.changes); receipts.set(request.id, { document: server, versions: new Map([['canvas', ++version]]) }); return { outcome: 'applied' }; }, // 文档回执查询与上传回执表分开,存在才表示该文档请求已应用。 async query(request) { return receipts.has(request.id) ? { outcome: 'applied' } : { outcome: 'unresolved' }; }, @@ -98,6 +99,7 @@ test('automatic image sizing publishes with creation history across intervening engine.editor.dispatch({ type: 'element.data', target, patch: { title: 'Renamed before decode' } }); // 记录解码前栈内容;完成后必须还是这几个历史 ID,尺寸补偿不能另塞一个用户撤销条目。 const undo = [...engine.getSnapshot().history.undo]; gate.resolve(); await engine.settle(); + // 解码宽高比 4:2,沿用当前节点宽度 100 得到高度 50;像素元数据不会把节点直接改为 4×2。 const root = engine.getSnapshot(); assert.equal(root.document.elements.get('picture').geometry.height, 50); assert.equal(root.document.elements.get('picture').geometry.x, 70); assert.deepEqual(root.history.undo, undo); assert.equal(root.runtime.resources.get(ref.id).sizing.grants[0].status, 'applied'); @@ -128,6 +130,7 @@ test('automatic image sizing publishes with creation history across intervening }); // 用户已提交 resize 即永久撤销默认尺寸授权;即使在解码完成前 undo resize,也不能重新授权自动调整。 +// undo 恢复几何值,grant 则记录“用户已经接管尺寸”的事实;若一起回退 grant,迟到解码会再次抢走用户控制。 test('a committed user resize permanently revokes default sizing even if undone before decoding', async () => { const f = fixture(), gate = deferred(), entered = deferred(), engine = await f.open({ images: { decode: async () => { entered.resolve(); await gate.promise; return { width: 4, height: 2 }; } } }); try { @@ -361,6 +364,7 @@ const resourceClock=()=>{let now=1000;return{advance:ms=>{now+=ms;},now:()=>now, async function unreferenceImage(engine){assert.equal(engine.editor.dispatch({type:'element.remove',target:{id:'picture',incarnation:1}}).status,'applied');assert.equal(engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{title:'evict old image history'}}).status,'applied');await engine.settle();} // 无引用资源先保存 soft marker,再满 100ms 宽限期后把文件删除和 purge 墓碑一起保存;重开可用远端 URL,不重新上传。 +// purge 只回收本地 Blob,不删除已上传资产或 stored 身份;后续显示因此能回退 displayURL。 test('local collection persists a soft marker, waits retention and atomically deletes bytes with a v3 recovery tombstone',async()=>{ const f=fixture(),clock=resourceClock();let engine=await f.open({historyLimit:1,clock});const ref=addImage(engine);try{ await engine.settle();await unreferenceImage(engine);assert.equal(engine.getResourcePins().pins.has(ref.id),false); diff --git a/tests/headless/runs.test.mjs b/tests/headless/runs.test.mjs index 64245a9..1fdb504 100644 --- a/tests/headless/runs.test.mjs +++ b/tests/headless/runs.test.mjs @@ -137,6 +137,7 @@ test('AI admission orders prior and later edits, persists its job, and does not await f.clock.advance(1);await engine.settle(); const finished=engine.getSnapshot();assert.equal(finished.runtime.runs.get(run.id).status,'succeeded');assert.equal(finished.runtime.runs.get(run.id).cursor,4); assert.equal(finished.document.elements.get('a').data.text,'generated');assert.equal(finished.document.elements.get('a').geometry.x,200);assert.equal(finished.document.elements.get('future').data.opaque.keep,true);assert.equal(finished.history.undo.length,2); + // 两次用户编辑有可撤销的内容意图;run.start 另占账本操作。自动结果由事件协调接纳,不作为第三次用户编辑压栈。 assert.equal(finished.operations.size,2);assert.equal(finished.runtime.ledger.operations.size,3); const restored=decodeRecovery(f.storage.image(),registry);assert.equal(restored.runtime.runs.get(run.id).cursor,4);assert.equal(restored.runtime.runs.get(run.id).outputEvents[0].output.text,'generated'); await engine.close(); @@ -169,7 +170,7 @@ test('run event waits for outstanding receipts and retains a content conflict, o test('older run event cannot rewind a later canonical document receipt',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.finish(); engine.editor.dispatch({type:'element.geometry',target,patch:{x:500}});await engine.settle(); - // The server rejects its stale version first. Adopt an externally refreshed full base only after receipt resolution. + // finish 已推进服务端版本,旧版本的几何请求先取得明确拒绝;再接纳完整基线并重放本地几何,旧事件不能覆盖它。 const server=f.server();const refreshed=engine.refresh(server.document,server.versions);assert.equal(refreshed.status,'applied'); await f.clock.advance(0);await engine.settle(); assert.equal(engine.getSnapshot().document.elements.get('a').geometry.x,500);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'generated'); @@ -185,7 +186,8 @@ test('run durability failure prevents admission and close fences late poll respo assert.equal(engine.getSnapshot().rootRevision,revision);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'original'); }); -// 读、写和轮询共用容量为二的网络门;排队请求被 abort 后退出,剩余请求完成时活跃数归零。 +// 直接测试容量为二的网络门:排队请求被 abort 后退出,剩余请求完成时活跃数归零。 +// execute 是统一模拟工作,本例未装配 HTTP transport;它验证共用门的并发/取消机制,不单独证明每类网络入口都已接入。 test('transport gate bounds combined run/read/write capacity and drops aborted waiters',async()=>{ const gate=createNetworkGate(2),release=defer(),controller=new AbortController();let active=0,max=0; const execute=signal=>gate.run(signal,async()=>{max=Math.max(max,++active);await release.promise;active--;}); @@ -245,6 +247,7 @@ test('durable cancellation survives response loss and does not create content hi }); // 显式应用立即形成可撤销内容修改;结果未知时撤销仍保留,恢复后先确认原应用再同步逆向编辑,redo 不重新生成或重复 apply。 +// history 保留的是选定输出造成的字段前后值;redo 走普通文档同步,不能再次调用可能产生新副作用的 run.start/run.apply。 test('explicit output application is immediate and undoable while unknown; recovery and redo never regenerate',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.finish('chosen output',true);await f.clock.advance(0);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0];assert.equal(run.status,'conflict'); f.loseApply=true;const applied=engine.applyRunOutput(run.id,target,id(999));assert.equal(applied.status,'applied');assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'chosen output');assert.equal(engine.getSnapshot().history.undo.length,1);assert.equal(engine.getSnapshot().operations.get(applied.operationIds[0]).operationType,'run.apply');await engine.settle(); @@ -255,6 +258,7 @@ test('explicit output application is immediate and undoable while unknown; recov // 取消确认推进 controlEpoch;此前捕获的旧轮询不能回退 cancelled,也不能提前推进游标,下一轮新响应才消费取消事件。 +// controlEpoch 是控制状态的换代屏障,cursor 是已接纳事件序号;取消已确认时 cursor 仍可为 0,两者不应合成一个计数。 test('a poll captured before cancellation cannot regress the acknowledged control state',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0];f.pollGate=defer();await f.clock.advance(0);await tick(); engine.cancelRun(run.id);await tick();assert.equal(engine.getSnapshot().runtime.runs.get(run.id).status,'cancelled');assert.equal(engine.getSnapshot().runtime.runs.get(run.id).controlEpoch,1);