feat: start Week Ahead days early (Closes #1224)
CI / lint (pull_request) Successful in 3m53s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m38s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-21 14:37:50 +00:00
1 parent 25597d1f05
commit f64ce3acc4
8 files changed
+212 -11

No files matched your search

+66
View File
@@ -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():
+70
View File
@@ -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():