feat: batch queued planning edits (#375)
CI / lint (pull_request) Successful in 44s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-09 06:05:40 +00:00
1 parent 5894eafb3f
commit 8f7a836d28
13 files changed
+353 -76

No files matched your search

+36 -7
View File
@@ -65,6 +65,27 @@ def test_deferrals_are_durable_revisioned_idempotent_and_account_scoped(tmp_path
) == {"revision": 2, "records": {}}
def test_batch_applies_in_one_ordered_idempotent_unit(tmp_path):
store = LaterStore(tmp_path / "later.sqlite3")
operations = [
{"operation_id": "first", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-10T09:00:00.000Z"},
{"operation_id": "second", "action": "restore", "item_id": "issue:r:1:"},
]
result = store.apply_batch("timmy", operations)
assert result == {
"revision": 2,
"records": {},
"accepted_operation_ids": ["first", "second"],
"duplicate_operation_ids": [],
"rejected_operations": [],
}
replay = store.apply_batch("timmy", operations)
assert replay["revision"] == 2
assert replay["accepted_operation_ids"] == []
assert replay["duplicate_operation_ids"] == ["first", "second"]
@pytest.mark.anyio
async def test_authenticated_later_api_uses_confirmed_account_and_csrf(monkeypatch, tmp_path):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
@@ -98,10 +119,10 @@ async def test_authenticated_later_api_uses_confirmed_account_and_csrf(monkeypat
changed = await client.patch(
"/api/v1/later",
json={
"operation_id": "mobile-1",
"action": "defer",
"item_id": "issue:stackchain/dashboard:363:",
"wake_at": "2026-08-10T09:00:00.000Z",
"operations": [
{"operation_id": "mobile-1", "action": "defer", "item_id": "issue:stackchain/dashboard:363:", "wake_at": "2026-08-10T09:00:00.000Z"},
{"operation_id": "mobile-2", "action": "defer", "item_id": "issue:stackchain/dashboard:365:", "wake_at": "2026-08-11T09:00:00.000Z"},
],
},
headers={
"Origin": "https://test",
@@ -112,10 +133,18 @@ async def test_authenticated_later_api_uses_confirmed_account_and_csrf(monkeypat
assert forbidden.status_code == 403
assert changed.status_code == 200
assert changed.json() == fetched.json() == {
"revision": 1,
assert changed.json() == {
"revision": 2,
"records": {
"issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z"
"issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z",
"issue:stackchain/dashboard:365:": "2026-08-11T09:00:00.000Z",
},
"accepted_operation_ids": ["mobile-1", "mobile-2"],
"duplicate_operation_ids": [],
"rejected_operations": [],
}
assert fetched.json() == {"revision": 2, "records": {
"issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z",
"issue:stackchain/dashboard:365:": "2026-08-11T09:00:00.000Z",
}}
assert fetched.headers["cache-control"] == "no-store"
+29 -10
View File
@@ -28,7 +28,7 @@ const values=new Map();const storage={{get length(){{return values.size}},key:i=
const channels=[];const channelFactory=()=>{{const channel={{onmessage:null,postMessage:data=>channels.filter(x=>x!==channel).forEach(x=>x.onmessage?.({{data}})),close(){{}}}};channels.push(channel);return channel}};
const held=new Set();const locks={{request:async(name,_options,work)=>{{if(held.has(name))return work(null);held.add(name);try{{return await work({{name}})}}finally{{held.delete(name)}}}}}};
let sequence=0,releaseFirst;const delivered=[];
const make=tab=>createLaterSync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}),fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,records:{{}}}};const operation=JSON.parse(options.body);delivered.push(operation);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,records:{{[operation.item_id]:operation.wake_at}}}}}},onRemoteRecords:()=>{{}},onStatus:()=>{{}}}});
const make=tab=>createLaterSync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}),fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,records:{{}}}};const operation=JSON.parse(options.body).operations[0];delivered.push(operation);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,records:{{[operation.item_id]:operation.wake_at}}}}}},onRemoteRecords:()=>{{}},onStatus:()=>{{}}}});
const first=make('first'),second=make('second');first.enqueue('defer','issue:r:1:','2026-08-10T09:00:00.000Z');
(async()=>{{const draining=first.flush();while(!releaseFirst)await Promise.resolve();second.enqueue('defer','issue:r:2:','2026-08-11T09:00:00.000Z');const competing=second.flush();releaseFirst();await Promise.all([draining,competing]);await first.flush();process.stdout.write(JSON.stringify({{delivered,pending:first.pending(),keys:[...values.keys()]}}));}})();
"""
@@ -46,7 +46,7 @@ const values=new Map();const storage={{get length(){{return values.size}},key:i=
const prefix='stackchain.later-sync.v1.timmy.operation.';
values.set(prefix+'old',JSON.stringify({{queued_at:100,operation:{{operation_id:'old',action:'defer',item_id:'issue:r:2:',wake_at:'2026-08-10T09:00:00.000Z'}}}}));
values.set(prefix+'new',JSON.stringify({{queued_at:101,operation:{{operation_id:'new',action:'restore',item_id:'issue:r:2:',wake_at:null}}}}));
const delivered=[];const sync=createLaterSync({{storage,getLogin:()=> 'timmy',fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,records:{{}}}};delivered.push(JSON.parse(options.body));return {{revision:1,records:{{}}}}}},onRemoteRecords:()=>{{}},onStatus:()=>{{}}}});
const delivered=[];const sync=createLaterSync({{storage,getLogin:()=> 'timmy',fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,records:{{}}}};delivered.push(...JSON.parse(options.body).operations);return {{revision:1,records:{{}}}}}},onRemoteRecords:()=>{{}},onStatus:()=>{{}}}});
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{delivered,pending:sync.pending(),keys:[...values.keys()]}}));}})();
"""
result = run_node(script)
@@ -67,7 +67,7 @@ const sync = createLaterSync({{
fetchJson:async (url,options={{}})=>{{
requests.push({{url,body:options.body&&JSON.parse(options.body)}});
if (!options.method) return remote;
const operation=JSON.parse(options.body);
const operation=JSON.parse(options.body).operations[0];
remote={{revision:2,records:{{...remote.records,[operation.item_id]:operation.wake_at}}}};
return remote;
}},
@@ -84,19 +84,38 @@ sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z');
assert [request["url"] for request in result["requests"]] == [
"api/v1/later",
"api/v1/later",
"api/v1/later",
]
assert result["requests"][1]["body"] == {
assert result["requests"][0]["body"] == {"operations": [{
"operation_id": "offline-op",
"action": "defer",
"item_id": "issue:r:2:",
"wake_at": "2026-08-10T09:00:00.000Z",
}
}]}
assert result["records"]["issue:r:2:"] == "2026-08-10T09:00:00.000Z"
assert result["status"] == "saved"
assert result["pending"] == []
def test_pending_later_edits_are_sent_as_one_batch_without_a_preflight_get():
script = f"""
const createLaterSync=require({json.dumps(str(LATER_SYNC))});
const values=new Map();const requests=[];let sequence=0;
const sync=createLaterSync({{
storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence),
fetchJson:async(_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:2,records:{{}},accepted_operation_ids:body.operations.map(x=>x.operation_id),duplicate_operation_ids:[],rejected_operations:[]}}}},
onRemoteRecords:()=>{{}},onStatus:()=>{{}},
}});
sync.enqueue('defer','issue:r:1:','2026-08-10T09:00:00.000Z');
sync.enqueue('restore','issue:r:2:');
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));}})();
"""
result=run_node(script)
assert len(result["requests"]) == 1
assert [item["operation_id"] for item in result["requests"][0]["operations"]] == ["op-1", "op-2"]
assert result["pending"] == []
def test_latest_offline_intent_wins_and_failed_delivery_stays_pending():
script = f"""
const createLaterSync = require({json.dumps(str(LATER_SYNC))});
@@ -145,7 +164,7 @@ sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z');
assert run_node(script) == {
"scheduledBeforeSwitch": 1,
"timers": 1,
"requests": ["GET"],
"requests": ["PATCH"],
"pendingForAlexander": [],
}
@@ -159,7 +178,7 @@ const sync=createLaterSync({{
getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence),
fetchJson:async (_url,options={{}})=>{{
if (!options.method) return {{revision:0,records:{{}}}};
const operation=JSON.parse(options.body); actions.push(operation.action);
const operation=JSON.parse(options.body).operations[0]; actions.push(operation.action);
if (operation.action==='defer') await new Promise(resolve=>releaseFirst=resolve);
return {{revision:actions.length,records:operation.action==='defer'?{{[operation.item_id]:operation.wake_at}}:{{}}}};
}},onRemoteRecords:r=>{{globalThis.records=r}},onStatus:()=>{{}},
@@ -214,7 +233,7 @@ sync.startLifecycle({{window:{{addEventListener:(n,h)=>handlers[n]=h}},document:
assert run_node(script) == {
"first": True,
"second": False,
"requests": ["GET", "PATCH"],
"requests": ["PATCH"],
"pending": [],
}
@@ -269,5 +288,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v53" in worker
assert "stackchain-dashboard-shell-v54" in worker
+1 -1
View File
@@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v53" in worker
assert "stackchain-dashboard-shell-v54" in worker
+6 -6
View File
@@ -108,7 +108,7 @@ async function dispatchNotificationClick(route) {{
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -116,14 +116,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -132,21 +132,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/update-ownership.js'" in source
+41 -6
View File
@@ -77,6 +77,36 @@ def test_limit_is_atomic_and_remove_frees_capacity(tmp_path):
]
def test_batch_applies_ordered_operations_once_and_rejects_only_capacity_conflicts(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3", limit=2)
result = store.apply_batch(
"timmy",
[
{"operation_id": "one", "action": "add", "item_id": "issue:r:1:"},
{"operation_id": "two", "action": "add", "item_id": "issue:r:2:"},
{"operation_id": "full", "action": "add", "item_id": "issue:r:3:"},
{"operation_id": "remove", "action": "remove", "item_id": "issue:r:1:"},
{"operation_id": "three", "action": "add", "item_id": "issue:r:3:"},
],
)
assert result == {
"revision": 4,
"ids": ["issue:r:2:", "issue:r:3:"],
"accepted_operation_ids": ["one", "two", "remove", "three"],
"duplicate_operation_ids": [],
"rejected_operations": [{"operation_id": "full", "reason": "today_full"}],
}
replay = store.apply_batch("timmy", [
{"operation_id": "two", "action": "add", "item_id": "issue:r:2:"},
{"operation_id": "three", "action": "add", "item_id": "issue:r:3:"},
])
assert replay["revision"] == 4
assert replay["accepted_operation_ids"] == []
assert replay["duplicate_operation_ids"] == ["two", "three"]
@pytest.mark.anyio
async def test_authenticated_today_api_uses_confirmed_account_and_csrf(monkeypatch, tmp_path):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
@@ -109,9 +139,10 @@ async def test_authenticated_today_api_uses_confirmed_account_and_csrf(monkeypat
changed = await client.patch(
"/api/v1/today",
json={
"operation_id": "mobile-1",
"action": "add",
"item_id": "issue:stackchain/dashboard:357:",
"operations": [
{"operation_id": "mobile-1", "action": "add", "item_id": "issue:stackchain/dashboard:357:"},
{"operation_id": "mobile-2", "action": "add", "item_id": "issue:stackchain/dashboard:359:"},
],
},
headers={
"Origin": "https://test",
@@ -122,8 +153,12 @@ async def test_authenticated_today_api_uses_confirmed_account_and_csrf(monkeypat
assert forbidden.status_code == 403
assert changed.status_code == 200
assert changed.json() == fetched.json() == {
"revision": 1,
"ids": ["issue:stackchain/dashboard:357:"],
assert changed.json() == {
"revision": 2,
"ids": ["issue:stackchain/dashboard:357:", "issue:stackchain/dashboard:359:"],
"accepted_operation_ids": ["mobile-1", "mobile-2"],
"duplicate_operation_ids": [],
"rejected_operations": [],
}
assert fetched.json() == {"revision": 2, "ids": ["issue:stackchain/dashboard:357:", "issue:stackchain/dashboard:359:"]}
assert fetched.headers["cache-control"] == "no-store"
+33 -17
View File
@@ -18,7 +18,7 @@ const channels=[]; const channelFactory=()=>{{const channel={{onmessage:null,pos
const held=new Set(); const locks={{request:async(name,_options,work)=>{{if(held.has(name))return work(null);held.add(name);try{{return await work({{name}})}}finally{{held.delete(name)}}}}}};
let sequence=0,releaseFirst;const delivered=[];
const make=tab=>createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}),
fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,ids:[]}};const operation=JSON.parse(options.body);delivered.push(operation.operation_id);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,ids:delivered}}}},onRemoteIds:()=>{{}},onStatus:()=>{{}}}});
fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,ids:[]}};const operation=JSON.parse(options.body).operations[0];delivered.push(operation.operation_id);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,ids:delivered}}}},onRemoteIds:()=>{{}},onStatus:()=>{{}}}});
const first=make('first'),second=make('second');first.enqueue('add','issue:r:1:');
(async()=>{{const draining=first.flush();while(!releaseFirst)await Promise.resolve();second.enqueue('add','issue:r:2:');const competing=second.flush();releaseFirst();await Promise.all([draining,competing]);await first.flush();process.stdout.write(JSON.stringify({{delivered,pending:first.pending(),keys:[...values.keys()]}}));}})();
"""
@@ -42,7 +42,7 @@ const sync = createTodaySync({{
getLogin: () => 'timmy', createOperationId: () => 'op-' + (++sequence),
fetchJson: async (_url, options={{}}) => {{
if (!options.method) return {{revision:0, ids:[]}};
const operation = JSON.parse(options.body);
const operation = JSON.parse(options.body).operations[0];
patches.push(operation);
if (patches.length === 1) {{
globalThis.firstPatchStarted();
@@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v53" in source
assert "stackchain-dashboard-shell-v54" in source
assert "BASE + 'static/today-sync.js'" in source
@@ -108,7 +108,7 @@ const sync = createTodaySync({{
fetchJson: async (url, options={{}}) => {{
requests.push({{url, body: options.body && JSON.parse(options.body)}});
if (!options.method) return remote;
remote = {{revision: remote.revision + 1, ids:['issue:r:9:', options.body && JSON.parse(options.body).item_id]}};
remote = {{revision: remote.revision + 1, ids:['issue:r:9:', JSON.parse(options.body).operations[0].item_id]}};
return remote;
}},
onRemoteIds: ids => {{ globalThis.adopted = ids; }},
@@ -131,19 +131,37 @@ sync.enqueue('add', 'issue:r:2:');
assert [request["url"] for request in result["requests"]] == [
"api/v1/today",
"api/v1/today",
"api/v1/today",
]
assert result["requests"][1]["body"] == {
assert result["requests"][0]["body"] == {"operations": [{
"operation_id": "fixed-op",
"action": "add",
"item_id": "issue:r:2:",
"direction": None,
}
}]}
assert result["adopted"] == ["issue:r:9:", "issue:r:2:"]
assert result["status"] == "saved"
assert result["pending"] == []
def test_pending_today_edits_are_sent_as_one_batch_without_a_preflight_get():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); const requests=[]; let sequence=0;
const sync=createTodaySync({{
storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence),
fetchJson:async (_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:2,ids:['issue:r:1:','issue:r:2:'],accepted_operation_ids:body.operations.map(x=>x.operation_id),duplicate_operation_ids:[],rejected_operations:[]}}}},
onRemoteIds:()=>{{}},onStatus:()=>{{}},
}});
sync.enqueue('add','issue:r:1:');sync.enqueue('add','issue:r:2:');
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));}})();
"""
result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)
assert len(result["requests"]) == 1
assert [item["operation_id"] for item in result["requests"][0]["operations"]] == ["op-1", "op-2"]
assert result["pending"] == []
def test_failed_delivery_stays_pending_for_offline_replay():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
@@ -183,7 +201,7 @@ const sync = createTodaySync({{
fetchJson:async (_url,options={{}})=>{{
attempts += 1;
if (attempts === 1) {{ const error = new Error('busy'); error.status=503; error.retryAfter=2; throw error; }}
if (options.method) patches.push(JSON.parse(options.body));
if (options.method) patches.push(...JSON.parse(options.body).operations);
return options.method ? {{revision:1,ids:['issue:r:2:']}} : {{revision:0,ids:[]}};
}},
onRemoteIds:()=>{{}}, onStatus:(state,detail)=>statuses.push([state,detail?.delayMs||null]),
@@ -201,7 +219,7 @@ sync.enqueue('add','issue:r:2:');
)
assert result["scheduled"] == [2000]
assert result["attempts"] == 3
assert result["attempts"] == 2
assert result["patches"] == [{
"operation_id": "stable-op", "action": "add", "item_id": "issue:r:2:", "direction": None,
}]
@@ -277,8 +295,8 @@ const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin: () => 'timmy', createOperationId: () => 'sixth',
fetchJson: async (_url, options={{}}) => {{
if (options.method) {{ patch = true; const error = new Error('full'); error.status = 409; throw error; }}
return {{revision:5, ids:['1','2','3','4','5']}};
patch = true;
return {{revision:5, ids:['1','2','3','4','5'], accepted_operation_ids:[], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'sixth',reason:'today_full'}}]}};
}},
onRemoteIds:ids=>{{globalThis.ids=ids}}, onStatus:value=>{{globalThis.status=value}},
}});
@@ -307,11 +325,9 @@ const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin: () => 'timmy', createOperationId: () => 'op-' + (++sequence),
fetchJson: async (_url, options={{}}) => {{
if (!options.method) return {{revision:5, ids:['1','2','3','4','5']}};
const operation = JSON.parse(options.body);
patches.push(operation);
if (operation.action === 'add') {{ const error = new Error('full'); error.status = 409; throw error; }}
return {{revision:6, ids:['1','2','3','4']}};
const operations = JSON.parse(options.body).operations;
patches.push(...operations);
return {{revision:6, ids:['1','2','3','4'], accepted_operation_ids:['op-2'], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'op-1',reason:'today_full'}}]}};
}},
onRemoteIds: ids => {{ globalThis.ids = ids; }},
onStatus: status => statuses.push(status),
@@ -413,7 +429,7 @@ sync.startLifecycle({{
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == {"requests": ["GET", "PATCH"], "pending": []}
assert result == {"requests": ["PATCH"], "pending": []}
def test_foregrounding_a_stale_tab_refreshes_the_saved_plan():