docs(tests): clarify engine durability and reconciliation scenarios
This commit is contained in:
@@ -36,6 +36,7 @@ function storageFixture() {
|
||||
return fixture;
|
||||
}
|
||||
// 服务替身按请求 ID 去重,只在首次 submit 应用修改;query 查询既有结果,overrides 可替换任一协议边界。
|
||||
// 这里只模拟成功快照与去重,不校验真实服务端的版本前置条件或同 ID 异体请求;不能用此替身证明完整服务端协议正确。
|
||||
function persistenceFixture(overrides = {}) {
|
||||
let server = original, version = 1;
|
||||
const calls = [], receipts = new Map();
|
||||
@@ -149,6 +150,7 @@ test('canonical replay conflict preserves working content and the known receipt
|
||||
assert.equal(engine.getSnapshot().document.elements.get('a').geometry.x, 200);
|
||||
assert.equal(engine.getSnapshot().runtime.conflicts.size, 1); assert.equal(port.calls.length, 1);
|
||||
const request = [...engine.getSnapshot().runtime.ledger.requests.values()][0];
|
||||
// outcome 说明服务端已经执行;phase 说明本地协调尚未完成。重放冲突不能把已执行改成未执行,也不能释放 writer。
|
||||
assert.equal(request.outcome, 'applied'); assert.equal(request.phase, 'waiting');
|
||||
assert.ok(engine.getSnapshot().runtime.receipts.has(request.id));
|
||||
await engine.close();
|
||||
@@ -362,6 +364,7 @@ test('a pending write train preserves every business root and publishes one dura
|
||||
|
||||
// 保存队列挂起时记录调用方目标 generation;调度器可能再发布冻结请求,因此还要核对后续捕获前缀的确认回声。
|
||||
const requested=engine.getSnapshot().runtime.ledger.generation,saving=engine.persist();await entered.promise;await tick();const expected=engine.getSnapshot().runtime.ledger.generation,queued=engine.getSnapshot().rootRevision-start.rootRevision;assert.equal(sends,0);assert.equal(engine.getSnapshot().runtime.ledger.durableGeneration,start.runtime.ledger.durableGeneration);gate.resolve();await saving;unsubscribe();
|
||||
// durableGeneration 的确认也要发布为新 Root 并保存,因此镜像数量包含业务切点和确认切点;后者不会新增用户编辑。
|
||||
assert.deepEqual(markers,[...new Set([requested,expected])]);assert.equal(engine.getSnapshot().runtime.ledger.durableGeneration,expected);assert.equal(engine.getDurability().durableRootRevision,engine.getSnapshot().rootRevision);const rows=storage.saved.slice(saved);assert.equal(rows.length,queued+markers.length);assert.deepEqual(rows.map(image=>image.rootRevision),Array.from({length:queued+markers.length},(_,i)=>start.rootRevision+i+1));assert.deepEqual(rows.slice(0,8).map(image=>image.payload.document.elements[0].data.title),Array.from({length:8},(_,i)=>'train-'+i));assert.equal(decodeRecovery(storage.image(),registry).runtime.ledger.durableGeneration,expected);await tick();assert.equal(sends,1);
|
||||
}finally{gate.resolve();network.resolve();storage.commit=commit;await engine.close();}
|
||||
});
|
||||
@@ -384,6 +387,7 @@ test('an invalid receipt cannot advance confirmed generation and the original cu
|
||||
}finally{gate.resolve();network.resolve();storage.commit=commit;await engine.close();}
|
||||
});
|
||||
// 保存成功后因 owner 失效而无法发布确认时保留已验证回执;恢复 owner 后只补确认 Root,监听器重入 persist 也不能并发写盘或重写旧切点。
|
||||
// 手动翻转 leader.active 是内存替身的故障注入,用于隔离确认发布重试;不表示真实存储可以复活已失效的租约。
|
||||
test('a failed durability publication retains its verified receipt through retry without rewriting saved business cuts',async()=>{
|
||||
const storage=storageFixture(),port=persistenceFixture(),{engine}=await open(storage,port,{clock:idleClock()}),network=deferred(),commit=storage.commit;let lose=true;port.submit=async()=>{await network.promise;return{outcome:'unresolved'};};try{
|
||||
await engine.persist();const leader=storage.owner(),saved=storage.saved.length;storage.commit=async(...args)=>{const receipt=await commit(...args);if(lose&&args[1].image.rootRevision===engine.getSnapshot().rootRevision){lose=false;leader.active=false;}return receipt;};engine.editor.dispatch({type:'element.data',target,patch:{title:'publication-retry'}});await assert.rejects(engine.persist(),/storage_leader_lost/);const confirmed=storage.saved.at(-1);assert.equal(engine.getDurability().durableRootRevision,confirmed.rootRevision);assert.ok(confirmed.payload.runtime.ledger.generation>engine.getSnapshot().runtime.ledger.durableGeneration);const count=storage.saved.length;leader.active=true;let activeWrites=0,maxWrites=0;const nested=[];storage.commit=async(...args)=>{activeWrites++;maxWrites=Math.max(maxWrites,activeWrites);try{await tick();return await commit(...args);}finally{activeWrites--;}};const unsubscribe=engine.editor.subscribe((_,root)=>{if(root.runtime.ledger.durableGeneration===confirmed.payload.runtime.ledger.generation)nested.push(engine.persist());});await engine.persist();await Promise.all(nested);unsubscribe();assert.equal(maxWrites,1);assert.equal(storage.saved.length,count+1);assert.equal(storage.saved.at(-1).rootRevision,confirmed.rootRevision+1);assert.equal(engine.getSnapshot().runtime.ledger.durableGeneration,confirmed.payload.runtime.ledger.generation);assert.equal(engine.getDurability().durableRootRevision,engine.getSnapshot().rootRevision);assert.ok(storage.saved.length>saved);
|
||||
@@ -409,6 +413,7 @@ test('an opted-in resolved receipt summary publishes with ACK/history/ledger and
|
||||
}finally{await engine.close();}
|
||||
});
|
||||
// 摘要准备后最终模型校验失败,必须保留原始成功回执和 waiting 占用;校验恢复后查询修复,不第二次 submit。
|
||||
// compactResolvedReceipt 首次调用时打开 blocked,使自定义 text 校验器拒绝 local;故障发生在本地发布,而非请求执行。
|
||||
test('a failed final publication keeps the raw receipt and waiting claim instead of the prepared summary',async()=>{
|
||||
let blocked=false,compactions=0;const custom=createRegistry(builtinComponents().map(definition=>definition.id==='text'?{...definition,validate:data=>typeof data.title==='string'&&typeof data.text==='string'&&(!blocked||data.title!=='local')}:definition)),storage=storageFixture(),port=persistenceFixture({compactResolvedReceipt(request){compactions++;if(compactions===1)blocked=true;return{summary:request.id};}});let{engine}=await open(storage,port,{registry:custom});
|
||||
try{engine.editor.dispatch({type:'element.data',target,patch:{title:'local'}});await engine.settle();const snapshot=engine.getSnapshot(),request=[...snapshot.runtime.ledger.requests.values()][0];assert.equal(compactions,1);assert.equal(request.outcome,'applied');assert.equal(request.phase,'waiting');assert.deepEqual(snapshot.runtime.receipts.get(request.id),{receipt:request.id});assert.equal(snapshot.document.elements.get('a').data.title,'local');assert.equal(snapshot.history.undo.length,1);blocked=false;await engine.close();({engine}=await open(storage,port,{registry:custom}));await engine.retry();await engine.settle();assert.equal(engine.getSnapshot().runtime.ledger.requests.get(request.id).phase,'resolved');assert.deepEqual(engine.getSnapshot().runtime.receipts.get(request.id),{summary:request.id});assert.equal(port.calls.filter(([kind])=>kind==='submit').length,1);assert.ok(port.calls.some(([kind])=>kind==='query'));
|
||||
|
||||
Reference in New Issue
Block a user