fix: preserve divergent synchronized Drafts (Closes #853)
CI / lint (pull_request) Successful in 1m53s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 22:29:37 +00:00
1 parent fca99fee86
commit cee3d93864
4 files changed
+260 -20

No files matched your search

+75
View File
@@ -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))});
+60
View File
@@ -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.",
}