feat: confirm capacity before Week-to-Today pulls (Closes #1244)
This commit is contained in:
1 parent
d01f502e70
commit
666671ee59
6 files changed
+132
-9
No files matched your search
@@ -39,7 +39,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
page.route("**/api/v1/today", lambda route: route.fulfill(
|
||||
status=200, content_type="application/json", body=json.dumps({
|
||||
"revision": 3, "ids": ["issue:acme/mobile:99:"],
|
||||
"capacity_minutes": 120, "estimates": {"issue:acme/mobile:99:": 30},
|
||||
"capacity_minutes": 60, "estimates": {"issue:acme/mobile:99:": 30},
|
||||
}),
|
||||
))
|
||||
|
||||
@@ -48,7 +48,8 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
pulled.append(body)
|
||||
route.fulfill(status=200, content_type="application/json", body=json.dumps({
|
||||
"today": {"revision": 4, "ids": ["issue:acme/mobile:99:", body["identity"]],
|
||||
"capacity_minutes": 120, "estimates": {body["identity"]: 45}},
|
||||
"capacity_minutes": 60,
|
||||
"estimates": {"issue:acme/mobile:99:": 30, body["identity"]: 45}},
|
||||
"week": {"revision": 99, "timezone": "UTC", "days": [{
|
||||
"plan_date": (date.today() + timedelta(days=1)).isoformat(),
|
||||
"ids": [], "capacity_minutes": 60, "estimates": {},
|
||||
@@ -319,9 +320,15 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
assert saved[-1]["days"][0]["ids"] == ["issue:acme/mobile:42:"]
|
||||
assert saved[-1]["days"][0]["capacity_minutes"] == 60
|
||||
pull = cards.first.locator('[data-week-pull-item="issue:acme/mobile:42:"]')
|
||||
expect(pull).to_have_text("Add to Today")
|
||||
expect(pull).to_have_text("Review Today · 75 of 60 min")
|
||||
pull_bounds = pull.bounding_box()
|
||||
assert pull_bounds and pull_bounds["height"] >= 44
|
||||
page.once("dialog", lambda dialog: dialog.dismiss())
|
||||
pull.click()
|
||||
expect(cards.first).to_contain_text("Polish desktop filters")
|
||||
assert pulled == [], "cancelling the overload review must not submit"
|
||||
expect(pull).to_have_text("Review Today · 75 of 60 min")
|
||||
page.once("dialog", lambda dialog: dialog.accept())
|
||||
pull.click()
|
||||
expect(cards.first).not_to_contain_text("Polish desktop filters")
|
||||
expect(page.locator("#week-review-status")).to_have_text(
|
||||
@@ -329,6 +336,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
)
|
||||
assert pulled and pulled[-1]["identity"] == "issue:acme/mobile:42:"
|
||||
assert pulled[-1]["today_revision"] == 3
|
||||
assert pulled[-1]["allow_over_capacity"] is True
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
browser.close()
|
||||
finally:
|
||||
|
||||
@@ -237,6 +237,47 @@ def test_pull_week_item_rejects_full_or_changed_today_without_partial_write(tmp_
|
||||
assert store.get_week("timmy") == week
|
||||
|
||||
|
||||
def test_pull_week_item_requires_explicit_today_capacity_overload_confirmation(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3", encryption_key=b"c" * 32,
|
||||
clock=lambda: datetime(2026, 8, 22, 8, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
active_id = "issue:stackchain/dashboard:1:"
|
||||
pulled_id = "issue:stackchain/dashboard:2:"
|
||||
seed = store.replace_week(
|
||||
"timmy", base_revision=0, timezone="UTC", days=[{
|
||||
"plan_date": "2026-08-22", "ids": [active_id],
|
||||
"capacity_minutes": 60, "estimates": {active_id: 45},
|
||||
}],
|
||||
)
|
||||
today = store.promote_week(
|
||||
"timmy", promotion_id="seed-today", week_revision=seed["revision"],
|
||||
plan_date="2026-08-22", today_revision=0,
|
||||
)
|
||||
week = store.replace_week(
|
||||
"timmy", base_revision=2, timezone="UTC", days=[{
|
||||
"plan_date": "2026-08-23", "ids": [pulled_id],
|
||||
"capacity_minutes": 60, "estimates": {pulled_id: 30},
|
||||
}],
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="explicit overload confirmation"):
|
||||
store.pull_week_item(
|
||||
"timmy", operation_id="unconfirmed", identity=pulled_id,
|
||||
today_revision=today["revision"], week_revision=week["revision"],
|
||||
)
|
||||
assert store.get("timmy") == today
|
||||
assert store.get_week("timmy") == week
|
||||
|
||||
moved = store.pull_week_item(
|
||||
"timmy", operation_id="confirmed", identity=pulled_id,
|
||||
today_revision=today["revision"], week_revision=week["revision"],
|
||||
allow_over_capacity=True,
|
||||
)
|
||||
assert moved["today"]["capacity_minutes"] == 60
|
||||
assert moved["today"]["estimates"] == {active_id: 45, pulled_id: 30}
|
||||
|
||||
|
||||
def test_normal_week_promotion_still_rejects_a_future_day_without_writes(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
|
||||
@@ -309,7 +309,7 @@ const week=createWeekPlan({fetchJson:async(url,options={})=>{
|
||||
week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['sibling'],capacity_minutes:90,estimates:{sibling:30}}]}};
|
||||
},localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
|
||||
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','sibling'],capacity_minutes:90,estimates:{pull:45,sibling:30}}]});
|
||||
const pulled=await week.pullItem('pull',{revision:4,ids:['active']},'stable-pull');
|
||||
const pulled=await week.pullItem('pull',{revision:4,ids:['active']},'stable-pull',true);
|
||||
console.log(JSON.stringify({pulled,requests,state:week.state()}));
|
||||
""")
|
||||
|
||||
@@ -318,6 +318,7 @@ console.log(JSON.stringify({pulled,requests,state:week.state()}));
|
||||
"body": {
|
||||
"operation_id": "stable-pull", "identity": "pull",
|
||||
"today_revision": 4, "week_revision": 7,
|
||||
"allow_over_capacity": True,
|
||||
},
|
||||
}]
|
||||
assert result["pulled"]["today"]["ids"] == ["active", "pull"]
|
||||
@@ -489,6 +490,61 @@ console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,ope
|
||||
assert result["editWeekLabel"] == "Edit week"
|
||||
|
||||
|
||||
def test_week_overview_reviews_today_capacity_before_confirming_an_overload():
|
||||
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:()=>{}});
|
||||
elements.set('#week-plan-dates',makeElement());
|
||||
const reviewDays=makeElement();
|
||||
Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.pullButtons=[...value.matchAll(/data-week-pull-item=\"([^\"]+)/g)].map(match=>({
|
||||
dataset:{weekPullItem:match[1]},listeners:{},disabled:false,
|
||||
addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}
|
||||
}));},get(){return this.value||'';}});
|
||||
reviewDays.querySelectorAll=selector=>selector==='[data-week-pull-item]'?reviewDays.pullButtons||[]:[];
|
||||
elements.set('#week-review-days',reviewDays);
|
||||
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'])
|
||||
if(!elements.has(selector))elements.set(selector,makeElement());
|
||||
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
|
||||
ids:index===0?['issue:stackchain/dashboard:44:']:[],capacity_minutes:60,
|
||||
estimates:index===0?{'issue:stackchain/dashboard:44:':30}:{},planned_minutes:index===0?30:0,overloaded:false})),
|
||||
duplicates:[],blockers:[],can_confirm:true};
|
||||
const pulls=[],confirmations=[];let approve=false;
|
||||
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
|
||||
pass:date=>({position:dates.indexOf(date)+1,total:7,planned:1,next_date:null,last:false}),load:async()=>({}),
|
||||
summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>({}),
|
||||
pullItem:async(id,today,operationId,allowOverCapacity)=>{pulls.push({id,operationId,allowOverCapacity});
|
||||
return {today:{revision:4,ids:['active',id],capacity_minutes:60,estimates:{active:45,[id]:30}},week:{revision:8,days:[]}};}};
|
||||
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),
|
||||
getItem:id=>({kind:'issue',title:'Capacity review',repository:'stackchain/dashboard',number:44}),
|
||||
openPlanner:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
|
||||
today:()=>({revision:3,ids:['active'],capacity_minutes:60,estimates:{active:45}}),operationId:()=> 'capacity-pull',
|
||||
confirmEarly:message=>{confirmations.push(message);return approve;},
|
||||
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||
await workflow.open({disabled:false});
|
||||
const markup=reviewDays.innerHTML,button=reviewDays.pullButtons[0];
|
||||
await button.listeners.click({currentTarget:button});
|
||||
const afterCancel={pulls:pulls.length,disabled:button.disabled,status:elements.get('#week-review-status').textContent};
|
||||
approve=true;
|
||||
await button.listeners.click({currentTarget:button});
|
||||
console.log(JSON.stringify({markup,confirmations,afterCancel,pulls}));
|
||||
""")
|
||||
|
||||
assert "75 of 60 min" in result["markup"]
|
||||
assert result["afterCancel"] == {"pulls": 0, "disabled": False, "status": "Week Ahead overview · no changes made."}
|
||||
assert result["confirmations"] == [
|
||||
"Add Capacity review to Today?\n\nToday will be 75 of 60 min · 15 min over capacity.",
|
||||
"Add Capacity review to Today?\n\nToday will be 75 of 60 min · 15 min over capacity.",
|
||||
]
|
||||
assert result["pulls"] == [{
|
||||
"id": "issue:stackchain/dashboard:44:", "operationId": "capacity-pull",
|
||||
"allowOverCapacity": True,
|
||||
}]
|
||||
|
||||
|
||||
def test_week_overview_removes_active_work_and_undo_restores_it():
|
||||
result = run_controller("""
|
||||
const createWorkflow=createWeekPlan.Workflow;
|
||||
|
||||
Reference in new issue
Block a user