feat: preserve Search work details offline (Closes #1220)
This commit is contained in:
1 parent
ba50784022
commit
2b874d8427
6 files changed
+151
-20
No files matched your search
@@ -137,6 +137,30 @@ const flow=createWeekBatchPlan({{
|
||||
]
|
||||
|
||||
|
||||
def test_week_batch_stages_details_only_for_successfully_placed_canonical_items():
|
||||
script = f"""
|
||||
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
|
||||
const flow=createSearchBatchPlan.createWeekBatchPlan({{
|
||||
week:{{load:async()=>{{}},dates:()=>[{{date:'2026-08-21'}}],day:()=>({{ids:[],estimates:{{}}}}),
|
||||
placement:()=>null,place:id=>!id.endsWith('#2'),
|
||||
rememberPendingItem:(id,item)=>calls.push(['details',id,item.title]),flush:async()=>calls.push(['flush'])}},
|
||||
prepare:async item=>({{...item,title:'Canonical '+item.number}}),
|
||||
identity:item=>item.repository+'#'+item.number,
|
||||
}});
|
||||
const calls=[];const items=[
|
||||
{{repository:'r',number:1,title:'Search one'}},{{repository:'r',number:2,title:'Search two'}}];
|
||||
(async()=>{{const outcome=await flow.run(items,{{'r#1':{{date:'2026-08-21',estimate:20}},'r#2':{{date:'2026-08-21',estimate:30}}}});
|
||||
process.stdout.write(JSON.stringify({{outcome,calls}}));}})();
|
||||
"""
|
||||
|
||||
result = run_node(script)
|
||||
assert result["outcome"] == {
|
||||
"status": "partial", "planned": 1,
|
||||
"failed": [{"id": "r#2", "reason": "assigned-not-planned"}],
|
||||
}
|
||||
assert result["calls"] == [["details", "r#1", "Canonical 1"], ["flush"]]
|
||||
|
||||
|
||||
def test_mobile_search_exposes_touch_safe_durable_batch_planning_controls():
|
||||
html = HTML.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
|
||||
@@ -99,3 +99,26 @@ console.log(JSON.stringify(outcome));
|
||||
""")
|
||||
|
||||
assert result == {"status": "planned-pending", "date": "2026-08-22", "estimate": 45}
|
||||
|
||||
|
||||
def test_search_week_plan_stages_canonical_details_before_flushing():
|
||||
result = run_planner("""
|
||||
const calls=[];
|
||||
const week={load:async()=>({}),dates:()=>[{date:'2026-08-22',label:'Sat'}],
|
||||
day:date=>({plan_date:date,ids:[],capacity_minutes:120,estimates:{}}),placement:()=>null,
|
||||
place:(id)=>{calls.push(['place',id]);return true;},
|
||||
rememberPendingItem:(id,item)=>{calls.push(['details',id,item.title,item.repository,item.number]);return true;},
|
||||
flush:async()=>{calls.push(['flush']);return {revision:3};}};
|
||||
const planner=createSearchWeekPlan({week,claim:async item=>({...item,title:'Canonical title'}),
|
||||
accept:item=>({...item,accepted:true}),identity:item=>`issue:${item.repository}:${item.number}:`});
|
||||
const outcome=await planner.plan({kind:'issue',state:'open',repository:'stackchain/dashboard',number:42,claimable:true},
|
||||
{date:'2026-08-22',estimate:45});
|
||||
console.log(JSON.stringify({outcome,calls}));
|
||||
""")
|
||||
|
||||
assert result["outcome"]["status"] == "planned"
|
||||
assert result["calls"] == [
|
||||
["place", "issue:stackchain/dashboard:42:"],
|
||||
["details", "issue:stackchain/dashboard:42:", "Canonical title", "stackchain/dashboard", 42],
|
||||
["flush"],
|
||||
]
|
||||
@@ -577,6 +577,58 @@ console.log(JSON.stringify({loaded,restored,item:fallback.item('issue:r:42:'),ot
|
||||
assert "secret" not in result["snapshot"]["items"]["issue:r:42:"]
|
||||
|
||||
|
||||
def test_week_controller_promotes_bounded_pending_work_details_after_matching_receipt():
|
||||
result = run_controller("""
|
||||
const values=new Map();
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
let online=true;const requests=[];
|
||||
const options={storage,getLogin:()=> 'timmy',now:()=>Date.parse('2026-08-20T12:34:00Z'),
|
||||
fetchJson:async(_url,request={})=>{
|
||||
requests.push(request.body?JSON.parse(request.body):null);
|
||||
if(!online)throw new Error('offline');
|
||||
if(request.method==='PUT')return {revision:8,timezone:'UTC',days:JSON.parse(request.body).days};
|
||||
return {revision:7,timezone:'UTC',days:[]};
|
||||
},localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
|
||||
const live=createWeekPlan(options);await live.load();
|
||||
live.place('issue:r:42:','2026-08-21',45);
|
||||
const admitted=live.rememberPendingItem('issue:r:42:',{kind:'issue',title:'T'.repeat(600),repository:'r'.repeat(300),
|
||||
number:42,body:'private body',token:'drop'});
|
||||
const queued=live.pending();
|
||||
await live.flush();
|
||||
const confirmed=JSON.parse(values.get('stackchain.week-confirmed.v1.timmy'));
|
||||
online=false;const offline=createWeekPlan(options);const restored=await offline.load();
|
||||
console.log(JSON.stringify({admitted,queued,confirmed,restored,item:offline.item('issue:r:42:'),requests}));
|
||||
""")
|
||||
|
||||
assert result["admitted"] is True
|
||||
assert result["queued"]["items"]["issue:r:42:"]["title"] == "T" * 500
|
||||
assert len(result["queued"]["items"]["issue:r:42:"]["repository"]) == 255
|
||||
assert result["requests"][1].keys() == {"base_revision", "timezone", "days"}
|
||||
assert result["confirmed"]["week"]["revision"] == 8
|
||||
assert result["confirmed"]["items"]["issue:r:42:"] == result["item"]
|
||||
assert result["item"]["number"] == 42
|
||||
assert "body" not in result["item"] and "token" not in result["item"]
|
||||
assert result["restored"]["offline_snapshot"] is True
|
||||
|
||||
|
||||
def test_week_controller_keeps_pending_work_details_across_a_cold_offline_reload():
|
||||
result = run_controller("""
|
||||
const values=new Map();
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
const options={storage,getLogin:()=> 'timmy',fetchJson:async()=>{throw new Error('offline');},
|
||||
localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
|
||||
const first=createWeekPlan(options);first.adopt({revision:7,timezone:'UTC',days:[]});
|
||||
first.place('issue:r:42:','2026-08-21',45);
|
||||
first.rememberPendingItem('issue:r:42:',{kind:'issue',title:'Pending work',repository:'r',number:42});
|
||||
const restored=createWeekPlan(options);const state=await restored.load();
|
||||
console.log(JSON.stringify({state,item:restored.item('issue:r:42:'),pending:restored.pending()}));
|
||||
""")
|
||||
|
||||
assert result["state"]["sync_pending"] is True
|
||||
assert result["item"] == {"kind": "issue", "title": "Pending work", "repository": "r", "number": 42}
|
||||
assert result["pending"]["items"]["issue:r:42:"] == result["item"]
|
||||
|
||||
|
||||
def test_week_controller_does_not_use_a_confirmed_snapshot_after_authentication_fails():
|
||||
result = run_controller("""
|
||||
const values=new Map();
|
||||
|
||||
Reference in new issue
Block a user