fix: preserve divergent synchronized Drafts (Closes #853)
This commit is contained in:
1 parent
fca99fee86
commit
cee3d93864
4 files changed
+260
-20
No files matched your search
@@ -145,6 +145,81 @@ function device() {{
|
||||
assert output["blockers"] == [{"repository": "o/api", "number": 7, "title": "API"}]
|
||||
|
||||
|
||||
def test_unfiled_captures_reconcile_remote_replaces_stale_content_and_evidence():
|
||||
script = f"""
|
||||
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
|
||||
const values=new Map(),blobs=new Map();
|
||||
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const attachmentStore={{get:id=>blobs.get(id),put:(id,value)=>blobs.set(id,value),delete:id=>blobs.delete(id)}};
|
||||
const captures=createUnfiledCaptures({{
|
||||
storage,attachmentStore,getCaptureLogin:()=>'timmy',getCurrentLogin:()=>'timmy',createId:()=>'field',now:()=>1,
|
||||
}});
|
||||
(async()=>{{
|
||||
await captures.save({{title:'Field failure',body:'phone edit',attachment:{{
|
||||
filename:'phone.png',contentType:'image/png',blob:new Blob(['phone'])
|
||||
}}}});
|
||||
await captures.reconcileRemote([{{
|
||||
id:'field',title:'Field failure',body:'desktop edit',saved_at:2,evidence:[{{
|
||||
filename:'desktop.png',content_type:'image/png',note:'newer',data:'ZGVza3RvcA=='
|
||||
}}]
|
||||
}}], 'timmy');
|
||||
const resumed=await captures.resume('field','timmy');
|
||||
process.stdout.write(JSON.stringify({{
|
||||
body:resumed.body,filename:resumed.attachments[0].filename,note:resumed.attachments[0].note,
|
||||
bytes:await resumed.attachments[0].blob.text(),count:captures.list().length,
|
||||
}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1)}});
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"body": "desktop edit",
|
||||
"filename": "desktop.png",
|
||||
"note": "newer",
|
||||
"bytes": "desktop",
|
||||
"count": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_unfiled_captures_reconcile_rolls_back_evidence_when_metadata_write_fails():
|
||||
script = f"""
|
||||
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
|
||||
const values=new Map(),blobs=new Map();let failWrite=false;
|
||||
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>{{if(failWrite)throw new Error('quota');values.set(k,v)}}}};
|
||||
const attachmentStore={{get:id=>blobs.get(id),put:(id,value)=>blobs.set(id,value),delete:id=>blobs.delete(id)}};
|
||||
const captures=createUnfiledCaptures({{
|
||||
storage,attachmentStore,getCaptureLogin:()=>'timmy',getCurrentLogin:()=>'timmy',createId:()=>'field',now:()=>1,
|
||||
}});
|
||||
(async()=>{{
|
||||
await captures.save({{title:'Field failure',body:'phone edit',attachment:{{
|
||||
filename:'phone.png',contentType:'image/png',blob:new Blob(['phone'])
|
||||
}}}});
|
||||
failWrite=true;let error='';
|
||||
try {{
|
||||
await captures.reconcileRemote([{{
|
||||
id:'field',title:'Field failure',body:'desktop edit',saved_at:2,evidence:[{{
|
||||
filename:'desktop.png',content_type:'image/png',data:'ZGVza3RvcA=='
|
||||
}}]
|
||||
}}], 'timmy');
|
||||
}} catch (caught) {{ error=caught.message; }}
|
||||
const resumed=await captures.resume('field','timmy');
|
||||
const proof=resumed.attachments?.[0] || resumed.attachment;
|
||||
process.stdout.write(JSON.stringify({{
|
||||
error,body:resumed.body,filename:proof.filename,
|
||||
bytes:await proof.blob.text(),
|
||||
}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1)}});
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"error": "quota",
|
||||
"body": "phone edit",
|
||||
"filename": "phone.png",
|
||||
"bytes": "phone",
|
||||
}
|
||||
|
||||
|
||||
def test_unfiled_capture_restores_selected_blockers():
|
||||
script = f"""
|
||||
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
|
||||
|
||||
@@ -102,3 +102,63 @@ const sync=createSync({{
|
||||
assert output["puts"] == 1
|
||||
assert output["remote"]["drafts"][0]["id"] == "local"
|
||||
assert output["results"] == [True, True]
|
||||
|
||||
|
||||
def test_unfiled_draft_sync_preserves_divergent_same_draft_edits_as_a_conflict_copy():
|
||||
script = f"""
|
||||
const createSync=require({json.dumps(str(SYNC))});
|
||||
let remote={{revision:0,drafts:[]}};
|
||||
async function api(_url,init) {{
|
||||
if(!init) return JSON.parse(JSON.stringify(remote));
|
||||
const body=JSON.parse(init.body);
|
||||
if(body.revision!==remote.revision) {{
|
||||
const error=new Error('conflict');error.status=409;error.payload={{detail:{{snapshot:remote}}}};throw error;
|
||||
}}
|
||||
remote={{revision:remote.revision+1,drafts:body.drafts}};
|
||||
return JSON.parse(JSON.stringify(remote));
|
||||
}}
|
||||
function device(initial=[]) {{
|
||||
let drafts=JSON.parse(JSON.stringify(initial));
|
||||
const values=new Map(), states=[];
|
||||
const captures={{
|
||||
exportOwned:async()=>JSON.parse(JSON.stringify(drafts)),
|
||||
mergeRemote:async items=>{{for(const item of items)if(!drafts.some(x=>x.id===item.id))drafts.push(JSON.parse(JSON.stringify(item))) }},
|
||||
reconcileRemote:async items=>{{drafts=JSON.parse(JSON.stringify(items))}},
|
||||
discard:async id=>{{drafts=drafts.filter(item=>item.id!==id);return true}},
|
||||
}};
|
||||
const sync=createSync({{
|
||||
captures,fetchJson:api,
|
||||
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}},
|
||||
createConflictId:()=> 'field-conflict-phone',onState:state=>states.push(state),
|
||||
}});
|
||||
return {{sync,states,edit(body,saved_at){{drafts[0].body=body;drafts[0].saved_at=saved_at}},drafts:()=>drafts}};
|
||||
}}
|
||||
(async()=>{{
|
||||
const original={{id:'field',title:'Field failure',body:'original',saved_at:1}};
|
||||
const phone=device([original]);
|
||||
await phone.sync.sync('timmy');
|
||||
const desktop=device();
|
||||
await desktop.sync.sync('timmy');
|
||||
desktop.edit('desktop edit',2);
|
||||
await desktop.sync.sync('timmy');
|
||||
await phone.sync.sync('timmy');
|
||||
phone.edit('phone edit',3);
|
||||
desktop.edit('desktop second edit',4);
|
||||
await desktop.sync.sync('timmy');
|
||||
await phone.sync.sync('timmy');
|
||||
process.stdout.write(JSON.stringify({{remote,phone:phone.drafts(),states:phone.states}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1)}});
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["remote"]["revision"] == 4
|
||||
assert [(item["id"], item["body"]) for item in output["remote"]["drafts"]] == [
|
||||
("field", "desktop second edit"),
|
||||
("field-conflict-phone", "phone edit"),
|
||||
]
|
||||
assert output["remote"]["drafts"][1]["title"] == "Field failure (conflict copy)"
|
||||
assert output["phone"] == output["remote"]["drafts"]
|
||||
assert output["states"][-1] == {
|
||||
"status": "conflict-preserved",
|
||||
"message": "Draft changed on two devices · both versions were saved.",
|
||||
}
|
||||
Reference in new issue
Block a user