docs(tests): explain migration retries and explicit conflict decisions
This commit is contained in:
@@ -63,6 +63,7 @@ test('keep-local freezes a new conditional intent/key, retains the old rejection
|
||||
const f=fixture(),engine=await f.open();title(engine,'local');await engine.settle();f.remote({title:'remote',text:'untouched remote'});
|
||||
const before=engine.getSnapshot(),old=[...before.runtime.ledger.requests.values()][0],session=await engine.prepareRecovery(),plan=session.plan(choices(engine));
|
||||
assert.equal(engine.getSnapshot(),before);assert.equal(plan.document.elements.get('a').data.title,'local');assert.equal(plan.document.elements.get('a').data.text,'untouched remote');
|
||||
// keep-local 是基于远程 before 值重新表达意图,不是重发旧请求;旧冻结 body 与 not-applied 证据应继续留存。
|
||||
const replacement=plan.operations.get(plan.operationIds[0]);assert.notEqual(replacement.id,old.operationIds[0]);assert.equal(replacement.changes[0].before,'remote');
|
||||
assert.equal(plan.ledger.requests.get(old.id).body,old.body);assert.equal(plan.ledger.operations.get(old.operationIds[0]).resolution,'superseded');
|
||||
f.reject=false;assert.equal((await session.resolve(plan)).status,'applied');await engine.settle();
|
||||
@@ -122,6 +123,7 @@ test('recovery publication failure keeps one plan and blocks sends until the exa
|
||||
const f=fixture(),engine=await f.open();title(engine,'local');await engine.settle();f.remote({title:'remote'});const session=await engine.prepareRecovery(),plan=session.plan(choices(engine));
|
||||
f.reject=false;f.broken=true;await assert.rejects(session.resolve(plan),/quota/);const revision=engine.getSnapshot().rootRevision;assert.equal(f.calls.length,1);
|
||||
await assert.rejects(session.apply(),/recovery_already_committed/);await assert.rejects(session.resolve({...plan}),/invalid_recovery_plan/);
|
||||
// 使用原 plan 对象重试保存,避免同一恢复决策被重复发布;持久屏障解除后才允许替代请求发送。
|
||||
f.broken=false;await session.resolve(plan);await engine.settle();assert.equal(f.calls.length,2);assert.ok(engine.getDurability().durableRootRevision>revision);assert.equal(engine.getSnapshot().runtime.recoveries.size,1);await engine.close();
|
||||
});
|
||||
|
||||
@@ -184,7 +186,7 @@ test('discard-only resolution rechecks current identity and a cancelled read can
|
||||
const f=fixture();let allowed=true,release,signal;const engine=await f.open();title(engine,'local');await engine.settle();
|
||||
const session=await engine.prepareRecovery(),plan=session.plan(choices(engine,'discard-local'));await session.cancel();
|
||||
const actualRead=f.port.read;f.port.read=async input=>{signal=input;await new Promise(resolve=>{release=resolve;});return actualRead(input);};const preparing=engine.prepareRecovery();await tick();await engine.cancelRecovery();assert.equal(signal.aborted,true);release();await assert.rejects(preparing,/recovery_session_closed/);assert.equal(engine.editor.can({type:'element.data',target,patch:{title:'more'}}).enabled,true);await engine.close();
|
||||
// The policy is evaluated again even when all chosen operations will be discarded.
|
||||
// 全部 discard 仍会改变本地历史/账本,提交时须再次检查策略;不能因没有新文档请求就跳过当前身份检查。
|
||||
const other=fixture(),guarded=await openProjectEngine({scope:{accountId:'account',workspaceId:'workspace',projectId:'project'},registry,document:original,versions:new Map([['canvas',1]]),storage:other.storage,persistence:other.port,policy:()=>allowed?{enabled:true}:{enabled:false,code:'identity_changed',targets:[]}});
|
||||
title(guarded,'local');await guarded.settle();const review=await guarded.prepareRecovery(),discard=review.plan(choices(guarded,'discard-local'));allowed=false;assert.equal((await review.resolve(discard)).code,'identity_changed');assert.equal(guarded.getSnapshot().runtime.recoveries.size,0);await review.cancel();await guarded.close();
|
||||
});
|
||||
@@ -200,6 +202,7 @@ test('later locally elided history survives keep/discard of an earlier rejected
|
||||
const localIds=[...created.operationIds,...removed.operationIds];assert.equal(f.calls.length,1);for(const id of localIds)assert.equal(engine.getSnapshot().runtime.ledger.operations.get(id).resolution,'elided');
|
||||
f.remote({title:'remote'});const session=await engine.prepareRecovery(),plan=session.plan(choices(engine,keep?'keep-local':'discard-local'));assert.equal(plan.document.tombstones.get(temporary.id),1);f.reject=false;await session.resolve(plan);await engine.settle();
|
||||
await engine.close();engine=await f.open();assert.equal(engine.getSnapshot().document.tombstones.get(temporary.id),1);assert.equal(engine.getSnapshot().runtime.acknowledged.tombstones.has(temporary.id),false);assert.equal(engine.getSnapshot().history.undo.length,keep?3:2);
|
||||
// localIds 已被本地抵消但仍有撤销历史;逐请求检查它们未被重新塞入网络同步,同时重开后墓碑仍有效。
|
||||
for(const request of engine.getSnapshot().runtime.ledger.requests.values())assert.equal(request.operationIds.some(id=>localIds.includes(id)),false);
|
||||
assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get(temporary.id).incarnation,2);
|
||||
assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.has(temporary.id),false);assert.equal(engine.getSnapshot().document.elements.get('a').data.title,keep?'pending':'remote');
|
||||
|
||||
@@ -73,6 +73,7 @@ test('lost committed migration ACK is recovered by original query after reload w
|
||||
// 提交前断线的 durable 请求先 query unknown,再保留原 ID 和包含额外空白的 body 精确重试,不能重序列化。
|
||||
test('a durable uncommitted request queries unknown then retries only its exact original body and key',async()=>{
|
||||
const f=fixture();f.failBeforeCommit=true;const first=f.make();await first.start(root,async()=>{});
|
||||
// 空白不会改变 JSON 语义,却会改变请求原字节;人为补空白后检查恢复转发原字符串,不能 parse 后重新 stringify。
|
||||
const original=first.read(),spaced={...original,body:' \n'+original.body+' '};f.storage.setItem(canvasMigrationStorageKey(scope),JSON.stringify(spaced));f.failBeforeCommit=false;
|
||||
const recovered=f.make({createId(){assert.fail('must retain original key');}});
|
||||
assert.equal((await recovered.resume()).status,'applied');
|
||||
@@ -108,6 +109,7 @@ test('bare rejection and malformed or foreign success retain the frozen request
|
||||
response=>({...response,body:{...response.body,identity_map:[{sourceCanvasId:child,structure:'node',sourceId:'a',elementId:id(20)}]}}),
|
||||
response=>({...response,body:{...response.body,identity_map:[{sourceCanvasId:root,structure:'connection',sourceId:'a',elementId:id(20)}]}}),
|
||||
]){
|
||||
// alter 只破坏返回给客户端的副本;假服务已保存合法回执,所以关闭篡改后可查询同一成功请求。
|
||||
const f=fixture();f.alter=alter;const migration=f.make();assert.equal((await migration.start(root,async()=>{})).status,'pending');
|
||||
const frozen=migration.read();assert.equal(frozen.phase,'pending');assert.equal(migrationBlocksCanvas(frozen,root),true);
|
||||
f.alter=undefined;assert.equal((await f.make().resume()).status,'applied');assert.equal(f.posts().length,1);
|
||||
@@ -147,6 +149,7 @@ test('failed result retirement remains recoverable and a storage port that drops
|
||||
const f=fixture(),storage={...f.storage,removeItem(){throw Error('storage unavailable');}};
|
||||
await assert.rejects(()=>f.make({storage}).start(root,async()=>{}),/storage unavailable/);
|
||||
assert.equal(f.records.size,1);assert.equal((await f.make().resume()).status,'applied');assert.equal(f.posts().length,1);
|
||||
// setItem 不抛错但吞掉写入,读回检查仍必须发现未持久化;不能把函数返回当作落盘成功证据。
|
||||
const g=fixture();await assert.rejects(()=>g.make({storage:{...g.storage,setItem(){}}}).start(root,async()=>{}),/migration_not_durable/);assert.equal(g.posts().length,0);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user