diff --git a/frontend/tomorrow-plan.js b/frontend/tomorrow-plan.js index 97c2629..a037ac0 100644 --- a/frontend/tomorrow-plan.js +++ b/frontend/tomorrow-plan.js @@ -63,14 +63,15 @@ function createTomorrowPlan({fetchJson,localDate,timeZone,storage,getLogin}={}) }).catch(async error=>{ if(error?.status===409) { const remote=await fetchJson('api/v1/tomorrow'); - lastConflict={local:pending(),remote:{...remote,ids:[...remote.ids],estimates:{...(remote.estimates||{})}}}; + lastConflict={key,local:pending(),remote:{...remote,ids:[...remote.ids],estimates:{...(remote.estimates||{})}}}; } throw error; }).finally(()=>{flushing=null;}); return flushing; } async function keepLocal() { - if(!lastConflict) return false; + if(!lastConflict||lastConflict.key!==storageKey()) return false; + const conflictKey=lastConflict.key; const local={...lastConflict.local,ids:[...lastConflict.local.ids],estimates:{...lastConflict.local.estimates}}; const body=deliveryBody({...local,base_revision:lastConflict.remote.revision}); try { @@ -84,13 +85,13 @@ function createTomorrowPlan({fetchJson,localDate,timeZone,storage,getLogin}={}) } catch(error) { if(error?.status===409) { const remote=await fetchJson('api/v1/tomorrow'); - lastConflict={local,remote:{...remote,ids:[...remote.ids],estimates:{...(remote.estimates||{})}}}; + lastConflict={key:conflictKey,local,remote:{...remote,ids:[...remote.ids],estimates:{...(remote.estimates||{})}}}; } throw error; } } function useRemote() { - if(!lastConflict) return false; + if(!lastConflict||lastConflict.key!==storageKey()) return false; const remote=lastConflict.remote; const key=storageKey(); if(key&&storage) storage.removeItem(key); @@ -162,8 +163,8 @@ function createTomorrowPlan({fetchJson,localDate,timeZone,storage,getLogin}={}) return {run,stop(){if(timer!==null)clearTimer(timer);timer=null;}}; } function conflict() { - return lastConflict&&{local:{...lastConflict.local,ids:[...lastConflict.local.ids],estimates:{...lastConflict.local.estimates}}, - remote:{...lastConflict.remote,ids:[...lastConflict.remote.ids],estimates:{...lastConflict.remote.estimates}}}; + return lastConflict?.key===storageKey()?{local:{...lastConflict.local,ids:[...lastConflict.local.ids],estimates:{...lastConflict.local.estimates}}, + remote:{...lastConflict.remote,ids:[...lastConflict.remote.ids],estimates:{...lastConflict.remote.estimates}}}:null; } return {adopt,load,save,stage,pending,flush,conflict,keepLocal,useRemote,promote,state,summary,nextLocalDate,startLifecycle}; } diff --git a/tests/test_tomorrow_plan_frontend.py b/tests/test_tomorrow_plan_frontend.py index 9283b0a..092bde8 100644 --- a/tests/test_tomorrow_plan_frontend.py +++ b/tests/test_tomorrow_plan_frontend.py @@ -167,6 +167,37 @@ console.log(JSON.stringify({adopted,requests,pending:planner.pending(),conflict: assert result["state"] == result["adopted"] +def test_tomorrow_conflict_cannot_be_read_or_resolved_after_account_switch(): + 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 login='timmy'; +const fetchJson=async (_url,options={})=>{ + if(options.method==='PUT'){const error=new Error('changed elsewhere');error.status=409;throw error;} + return {revision:12,ids:['issue:private:timmy:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'}; +}; +const planner=createTomorrowPlan({storage,getLogin:()=>login,fetchJson, + localDate:()=> '2026-08-19',timeZone:()=> 'UTC'}); +planner.adopt({revision:11,ids:[],capacity_minutes:null,estimates:{}}); +planner.stage({ids:['issue:private:phone:'],capacity_minutes:90,estimates:{}}); +try { await planner.flush(); } catch(_error) {} +const before=planner.conflict(); +login='alexander'; +const after=planner.conflict(); +const keep=await planner.keepLocal(); +const use=planner.useRemote(); +login='timmy'; +const restored=planner.conflict(); +console.log(JSON.stringify({before,after,keep,use,restored})); +""") + + assert result["before"]["remote"]["ids"] == ["issue:private:timmy:"] + assert result["after"] is None + assert result["keep"] is False + assert result["use"] is False + assert result["restored"]["local"]["ids"] == ["issue:private:phone:"] + + def test_tomorrow_planner_loads_and_saves_independently_from_today(): result = run_controller(""" const requests=[];