feat: start Week Ahead days early (Closes #1224)
This commit is contained in:
1 parent
25597d1f05
commit
f64ce3acc4
8 files changed
+212
-11
No files matched your search
@@ -165,6 +165,49 @@ def test_week_promotion_moves_only_due_date_to_today_exactly_once(tmp_path):
|
||||
assert remaining["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
def test_week_early_start_moves_a_future_day_to_empty_today_exactly_once(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
encryption_key=b"e" * 32,
|
||||
clock=lambda: datetime(2026, 8, 20, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
today = store.get("timmy")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
|
||||
promoted = store.promote_week(
|
||||
"timmy", promotion_id="early-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"], allow_future=True,
|
||||
)
|
||||
replay = store.promote_week(
|
||||
"timmy", promotion_id="early-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"], allow_future=True,
|
||||
)
|
||||
|
||||
assert replay == promoted
|
||||
assert promoted["ids"] == sample_days()[0]["ids"]
|
||||
assert promoted["plan_date"] == "2026-08-21"
|
||||
assert store.get_week("timmy")["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
def test_normal_week_promotion_still_rejects_a_future_day_without_writes(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
encryption_key=b"e" * 32,
|
||||
clock=lambda: datetime(2026, 8, 20, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
today = store.get("timmy")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
|
||||
with pytest.raises(main.TomorrowPlanNotDue):
|
||||
store.promote_week(
|
||||
"timmy", promotion_id="normal-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"],
|
||||
)
|
||||
|
||||
assert store.get("timmy") == today
|
||||
assert store.get_week("timmy") == week
|
||||
|
||||
|
||||
def test_week_promotion_preserves_nonempty_today_and_the_due_week(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
@@ -261,6 +304,29 @@ async def test_week_promotion_api_returns_both_preserved_plans_for_review(monkey
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_week_early_start_api_promotes_a_future_day(monkeypatch, tmp_path):
|
||||
async def user():
|
||||
return {"login": "timmy"}
|
||||
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3", encryption_key=b"a" * 32,
|
||||
clock=lambda: datetime(2026, 8, 20, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
today = store.get("timmy")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "_today_store", lambda: store)
|
||||
|
||||
result = await main.start_week_day_early(main.WeekPromotion(
|
||||
promotion_id="early-2026-08-21-r1", week_revision=week["revision"],
|
||||
plan_date="2026-08-21", today_revision=today["revision"],
|
||||
))
|
||||
|
||||
assert result["ids"] == sample_days()[0]["ids"]
|
||||
assert store.get_week("timmy")["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_week_reconciliation_api_returns_atomic_today_and_remaining_week(monkeypatch, tmp_path):
|
||||
async def user():
|
||||
|
||||
@@ -66,6 +66,37 @@ console.log(JSON.stringify({dates,saved,requests}));
|
||||
assert result["requests"][1]["body"]["days"][2]["ids"] == ["issue:r:3:"]
|
||||
|
||||
|
||||
def test_week_controller_starts_a_selected_future_day_early_and_refreshes_the_week():
|
||||
result = run_controller("""
|
||||
const requests=[];
|
||||
let loadCount=0;
|
||||
const day={plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}};
|
||||
const fetchJson=async(url,options={})=>{
|
||||
requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null});
|
||||
if(url==='api/v1/week/start-early')return {revision:3,...day,timezone:'UTC'};
|
||||
loadCount+=1;
|
||||
return loadCount===1?{revision:4,timezone:'UTC',days:[day]}:{revision:5,timezone:'UTC',days:[]};
|
||||
};
|
||||
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
|
||||
await week.load();
|
||||
const promoted=await week.startEarly('2026-08-21',2);
|
||||
console.log(JSON.stringify({promoted,requests,state:week.state()}));
|
||||
""")
|
||||
|
||||
assert result["promoted"]["ids"] == ["issue:r:1:"]
|
||||
assert result["requests"][1] == {
|
||||
"url": "api/v1/week/start-early",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"promotion_id": "early-2026-08-21-r4",
|
||||
"week_revision": 4,
|
||||
"plan_date": "2026-08-21",
|
||||
"today_revision": 2,
|
||||
},
|
||||
}
|
||||
assert result["state"]["days"] == []
|
||||
|
||||
|
||||
def test_week_controller_advances_through_one_continuous_seven_day_pass():
|
||||
result = run_controller("""
|
||||
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
|
||||
@@ -260,6 +291,39 @@ console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,ope
|
||||
assert result["editWeekLabel"] == "Edit week"
|
||||
|
||||
|
||||
def test_week_overview_confirms_and_starts_the_next_day_early_from_empty_today():
|
||||
result = run_controller("""
|
||||
const createWorkflow=createWeekPlan.Workflow;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const elements=new Map();
|
||||
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
|
||||
const root=makeElement();
|
||||
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.startButtons=[...value.matchAll(/data-week-start-early=\"([^\"]+)/g)].map(match=>({dataset:{weekStartEarly:match[1]},listeners:{},disabled:false,addEventListener(name,listener){this.listeners[name]=listener;}}));},get(){return this.value||'';}});
|
||||
root.querySelectorAll=selector=>selector==='[data-week-start-early]'?root.startButtons||[]:[];
|
||||
elements.set('#week-review-days',root);
|
||||
elements.set('#week-plan-dates',makeElement());
|
||||
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan','#week-offline-snapshot','#retry-week-live','#open-week-capacity-import'])if(!elements.has(selector))elements.set(selector,makeElement());
|
||||
const day={plan_date:'2026-08-21',label:'Friday, Aug 21',ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45},planned_minutes:75,overloaded:false};
|
||||
const review={days:dates.map((date,index)=>index===0?day:{plan_date:date,label:'Day '+(index+1),ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false}),duplicates:[],blockers:[],can_confirm:true};
|
||||
let request=null,confirmation='',replaced=null,refreshed=0;
|
||||
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(value=>value.plan_date===date),pass:()=>({}),load:async()=>({}),summary:()=> 'Nothing planned',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>{},startEarly:async(date,revision)=>{request={date,revision};return {ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45}};}};
|
||||
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,today:()=>({revision:7,ids:[]}),confirmEarly:message=>{confirmation=message;return true;},todayWork:{replace:ids=>{replaced=ids;},replacePlanning:()=>{}},refresh:()=>{refreshed+=1;},warm:()=>{}});
|
||||
await workflow.open({disabled:false});
|
||||
const button=root.startButtons[0];
|
||||
await button.listeners.click({currentTarget:button});
|
||||
console.log(JSON.stringify({buttonCount:root.startButtons.length,markup:root.innerHTML,confirmation,request,replaced,refreshed,status:elements.get('#my-work-action-status').textContent}));
|
||||
""")
|
||||
|
||||
assert result["buttonCount"] == 1
|
||||
assert "Start this day early" in result["markup"]
|
||||
assert "Friday, Aug 21" in result["confirmation"]
|
||||
assert "2 items · 75 of 90 min" in result["confirmation"]
|
||||
assert result["request"] == {"date": "2026-08-21", "revision": 7}
|
||||
assert result["replaced"] == ["one", "two"]
|
||||
assert result["refreshed"] == 1
|
||||
assert result["status"] == "Friday, Aug 21 is now Today."
|
||||
|
||||
|
||||
def test_week_overview_opens_known_work_with_the_originating_control_and_leaves_unknown_work_static():
|
||||
result = run_controller("""
|
||||
const createWorkflow=createWeekPlan.Workflow;
|
||||
@@ -325,10 +389,16 @@ def test_dashboard_routes_week_overview_controls_through_existing_detail_flow_wi
|
||||
|
||||
workflow_mount = dashboard.split("const weekFlow=createWeekPlanWorkflow({", 1)[1].split("});", 1)[0]
|
||||
assert "openItem:openRoutedWork" in workflow_mount
|
||||
assert "t:()=>latestTodayPlan" in workflow_mount
|
||||
|
||||
assert ".week-review-item-open" in css
|
||||
rule = css.split(".week-review-item-open", 1)[1].split("}", 1)[0]
|
||||
assert "min-height:44px" in rule
|
||||
assert "width:100%" in rule
|
||||
assert ".week-start-early" in css
|
||||
start_rule = css.split(".week-start-early", 1)[1].split("}", 1)[0]
|
||||
assert "min-height:44px" in start_rule
|
||||
assert "width:100%" in start_rule
|
||||
|
||||
|
||||
def test_week_workflow_marks_a_confirmed_fallback_read_only_and_retries_live_data():
|
||||
|
||||
Reference in new issue
Block a user