feat: reconcile Today with due Week Ahead work (Closes #1200)
CI / lint (pull_request) Successful in 3m32s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 4m51s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-21 00:08:43 +00:00
1 parent 5ac90d01e6
commit a10592c349
5 files changed
+280 -24

No files matched your search

+73
View File
@@ -186,6 +186,54 @@ def test_week_promotion_preserves_nonempty_today_and_the_due_week(tmp_path):
assert store.get_week("timmy") == week
def test_week_reconciliation_atomically_combines_today_and_consumes_only_due_day(tmp_path):
store = TodayStore(
tmp_path / "today.sqlite3", encryption_key=b"c" * 32,
clock=lambda: datetime(2026, 8, 22, 8, tzinfo=timezone.utc).timestamp(),
)
today = store.apply("timmy", "active", "add", "issue:r:active:")
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
result = store.reconcile_week(
"timmy", promotion_id="reconcile-2026-08-21-r1", week_revision=week["revision"],
plan_date="2026-08-21", today_revision=today["revision"],
ids=["issue:r:active:", "issue:secret/repo:3:"], capacity_minutes=120,
estimates={"issue:r:active:": 30, "issue:secret/repo:3:": 60},
)
replay = store.reconcile_week(
"timmy", promotion_id="reconcile-2026-08-21-r1", week_revision=week["revision"],
plan_date="2026-08-21", today_revision=today["revision"],
ids=["issue:r:active:", "issue:secret/repo:3:"], capacity_minutes=120,
estimates={"issue:r:active:": 30, "issue:secret/repo:3:": 60},
)
assert replay == result
assert result["today"]["ids"] == ["issue:r:active:", "issue:secret/repo:3:"]
assert result["week"]["days"] == [sample_days()[1]]
assert store.get("timmy") == result["today"]
assert store.get_week("timmy") == result["week"]
def test_week_reconciliation_rejects_work_outside_preserved_plans_without_partial_write(tmp_path):
store = TodayStore(
tmp_path / "today.sqlite3", encryption_key=b"c" * 32,
clock=lambda: datetime(2026, 8, 22, 8, tzinfo=timezone.utc).timestamp(),
)
today = store.apply("timmy", "active", "add", "issue:r:active:")
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
with pytest.raises(ValueError, match="selected work must come from Today or the due Week Ahead day"):
store.reconcile_week(
"timmy", promotion_id="bad", week_revision=week["revision"],
plan_date="2026-08-21", today_revision=today["revision"],
ids=["issue:other/repo:99:"], capacity_minutes=60,
estimates={"issue:other/repo:99:": 30},
)
assert store.get("timmy") == today
assert store.get_week("timmy") == week
@pytest.mark.anyio
async def test_week_promotion_api_returns_both_preserved_plans_for_review(monkeypatch, tmp_path):
async def user():
@@ -213,6 +261,31 @@ async def test_week_promotion_api_returns_both_preserved_plans_for_review(monkey
}
@pytest.mark.anyio
async def test_week_reconciliation_api_returns_atomic_today_and_remaining_week(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, 22, 8, tzinfo=timezone.utc).timestamp(),
)
today = store.apply("timmy", "active", "add", "issue:r:active:")
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.reconcile_week_plan(main.WeekReconciliation(
promotion_id="reconcile-2026-08-21-r1", week_revision=week["revision"],
plan_date="2026-08-21", today_revision=today["revision"],
ids=["issue:r:active:", "issue:secret/repo:3:"], capacity_minutes=120,
estimates={"issue:r:active:": 30, "issue:secret/repo:3:": 60},
))
assert result["today"]["ids"] == ["issue:r:active:", "issue:secret/repo:3:"]
assert result["week"]["days"] == [sample_days()[1]]
def test_existing_tomorrow_plan_migrates_into_week_without_losing_planning_data(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"m" * 32)
tomorrow = store.replace_tomorrow(
+43 -17
View File
@@ -572,32 +572,57 @@ console.log(JSON.stringify({promoted,requests}));
}]
def test_week_workflow_routes_blocked_rollover_into_today_review():
def test_week_controller_reconciles_reviewed_today_and_adopts_both_results():
result = run_controller("""
const requests=[];
const week=createWeekPlan({fetchJson:async(url,options={})=>{
const body=JSON.parse(options.body);requests.push({url,body});
return {today:{revision:5,ids:body.ids,capacity_minutes:body.capacity_minutes,estimates:body.estimates},
week:{revision:7,timezone:'UTC',days:[{plan_date:'2026-08-23',ids:['later'],capacity_minutes:30,estimates:{later:30}}]}};
},localDate:()=> '2026-08-23',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['due'],capacity_minutes:60,estimates:{due:30}}]});
const reconciled=await week.reconcile({today:{revision:4,ids:['unfinished']},week:week.state()},
{ids:['unfinished','due'],capacity_minutes:90,estimates:{unfinished:30,due:30}});
console.log(JSON.stringify({reconciled,requests,state:week.state()}));
""")
assert result["requests"][0]["url"] == "api/v1/week/reconcile"
assert result["requests"][0]["body"]["ids"] == ["unfinished", "due"]
assert result["requests"][0]["body"]["today_revision"] == 4
assert result["state"]["revision"] == 7
assert result["state"]["days"][0]["ids"] == ["later"]
assert result["reconciled"]["today"]["ids"] == ["unfinished", "due"]
def test_week_workflow_routes_blocked_rollover_into_atomic_combined_review():
result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const elements={'#mobile-week-summary':{textContent:''},'#my-work-action-status':{textContent:''}};
let opened=0,replaced=0,refreshed=0;
let opened=0,reconciled=null,replaced=[];
const blocked=new Error('blocked');blocked.code='week_today_in_progress';
const workflow=createWorkflow({
controller:{load:async()=>({}),promote:async()=>{throw blocked;},summary:()=> '1 item across 1 day'},
qs:selector=>elements[selector],getLogin:()=> 'timmy',openPlanner:()=>{opened+=1;},
blocked.today={revision:4,ids:['unfinished'],capacity_minutes:45,estimates:{unfinished:30}};
blocked.week={revision:6,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['due','unfinished'],capacity_minutes:90,estimates:{due:45,unfinished:30}}]};
const controller={load:async()=>({}),promote:async()=>{throw blocked;},state:()=>blocked.week,summary:()=> '1 item across 1 day',
reconcile:async(preserved,plan)=>{reconciled={preserved,plan};return {today:{revision:5,...plan},week:{revision:7,timezone:'UTC',days:[]}};}};
const workflow=createWorkflow({controller,qs:selector=>elements[selector],getLogin:()=> 'timmy',openPlanner:()=>{opened+=1;},
escapeHtml:value=>value,escapeAttribute:value=>value,
todayWork:{replace:()=>{replaced+=1;},replacePlanning:()=>{replaced+=1;}},
refresh:()=>{refreshed+=1;},warm:()=>{},
todayWork:{replace:value=>replaced.push(value),replacePlanning:value=>replaced.push(value)},refresh:()=>{},warm:()=>{},
});
const promoted=await workflow.promote({revision:4,ids:['unfinished']});
const retried=await workflow.promote({revision:4,ids:['unfinished']});
console.log(JSON.stringify({promoted,retried,opened,replaced,refreshed,status:elements['#my-work-action-status'].textContent}));
const review={copy:workflow.copy(),day:workflow.day()};
const saved=workflow.save({ids:['unfinished','due'],capacity_minutes:90,estimates:{unfinished:30,due:45}});
await new Promise(resolve=>setTimeout(resolve,0));
console.log(JSON.stringify({promoted,opened,review,saved,reconciled,replaced,status:elements['#my-work-action-status'].textContent}));
""")
assert result == {
"promoted": False,
"retried": False,
"opened": 1,
"replaced": 0,
"refreshed": 0,
"status": "Review unfinished Today before starting the saved Week Ahead day.",
}
assert result["promoted"] is False
assert result["opened"] == 1
assert result["review"]["copy"]["title"] == "Start today's plan"
assert result["review"]["day"]["ids"] == ["unfinished", "due"]
assert result["saved"] is True
assert result["reconciled"]["plan"]["ids"] == ["unfinished", "due"]
assert result["replaced"][0] == ["unfinished", "due"]
assert result["status"] == "Today started from unfinished and Week Ahead work."
def test_week_controller_does_not_promote_a_pending_week():
@@ -621,6 +646,7 @@ def test_mobile_week_ahead_entry_and_date_strip_are_touch_safe():
css = (FRONTEND / "dashboard.css").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
assert 'data-mobile-queue="week"' in index
assert 'id="week-plan-dates"' in index
assert '<script src="static/week-plan.js"></script>' in index