feat: pull Week Ahead work into Today (Closes #1242)
This commit is contained in:
1 parent
6afdad6fe4
commit
3fd5983df6
7 files changed
+314
-13
No files matched your search
@@ -34,6 +34,28 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
page = browser.new_page(viewport={"width": width, "height": height}, timezone_id="UTC")
|
||||
page_errors: list[str] = []
|
||||
page.on("pageerror", lambda error: page_errors.append(str(error)))
|
||||
pulled: list[dict] = []
|
||||
|
||||
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},
|
||||
}),
|
||||
))
|
||||
|
||||
def pull_item_route(route):
|
||||
body = json.loads(route.request.post_data or "{}")
|
||||
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}},
|
||||
"week": {"revision": 99, "timezone": "UTC", "days": [{
|
||||
"plan_date": (date.today() + timedelta(days=1)).isoformat(),
|
||||
"ids": [], "capacity_minutes": 60, "estimates": {},
|
||||
}]},
|
||||
}))
|
||||
|
||||
page.route("**/api/v1/week/pull-item", pull_item_route)
|
||||
|
||||
def week_route(route):
|
||||
if route.request.method == "PUT":
|
||||
@@ -66,6 +88,8 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
|
||||
page.locator("#submit-sign-in").click()
|
||||
page.wait_for_url(origin + "/", wait_until="networkidle")
|
||||
if page.locator("#plan-today-sheet").is_visible():
|
||||
page.locator("#cancel-plan-today").click()
|
||||
page.locator('[data-mobile-task="queues"]').click()
|
||||
page.locator('[data-mobile-queue="week"]').click()
|
||||
page.wait_for_timeout(100)
|
||||
@@ -294,6 +318,17 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
assert len(saved) == before_retirement + 1
|
||||
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")
|
||||
pull_bounds = pull.bounding_box()
|
||||
assert pull_bounds and pull_bounds["height"] >= 44
|
||||
pull.click()
|
||||
expect(cards.first).not_to_contain_text("Polish desktop filters")
|
||||
expect(page.locator("#week-review-status")).to_have_text(
|
||||
"Polish desktop filters added to Today."
|
||||
)
|
||||
assert pulled and pulled[-1]["identity"] == "issue:acme/mobile:42:"
|
||||
assert pulled[-1]["today_revision"] == 3
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
browser.close()
|
||||
finally:
|
||||
|
||||
@@ -189,6 +189,54 @@ def test_week_early_start_moves_a_future_day_to_empty_today_exactly_once(tmp_pat
|
||||
assert store.get_week("timmy")["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
def test_pull_week_item_atomically_appends_to_active_today_and_replays_exactly_once(tmp_path):
|
||||
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"i" * 32)
|
||||
active_id = "issue:stackchain/dashboard:1:"
|
||||
pulled_id = "issue:secret/repo:3:"
|
||||
today = store.apply("timmy", "seed-active", "add", active_id)
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
|
||||
result = store.pull_week_item(
|
||||
"timmy", operation_id="pull-secret-3", identity=pulled_id,
|
||||
today_revision=today["revision"], week_revision=week["revision"],
|
||||
)
|
||||
replay = store.pull_week_item(
|
||||
"timmy", operation_id="pull-secret-3", identity=pulled_id,
|
||||
today_revision=today["revision"], week_revision=week["revision"],
|
||||
)
|
||||
|
||||
assert replay == result
|
||||
assert result["today"]["ids"] == [active_id, pulled_id]
|
||||
assert result["today"]["estimates"] == {pulled_id: 60}
|
||||
assert result["week"]["days"][0]["ids"] == ["issue:secret/repo:2:"]
|
||||
assert result["week"]["days"][0]["estimates"] == {"issue:secret/repo:2:": 45}
|
||||
assert result["week"]["days"][1] == sample_days()[1]
|
||||
assert store.get("timmy") == result["today"]
|
||||
assert store.get_week("timmy") == result["week"]
|
||||
|
||||
|
||||
def test_pull_week_item_rejects_full_or_changed_today_without_partial_write(tmp_path):
|
||||
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"f" * 32)
|
||||
today = store.get("timmy")
|
||||
for index in range(5):
|
||||
today = store.apply("timmy", f"seed-{index}", "add", f"issue:r:{index + 1}:")
|
||||
week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC")
|
||||
|
||||
with pytest.raises(main.TodayPlanFull):
|
||||
store.pull_week_item(
|
||||
"timmy", operation_id="full", identity="issue:secret/repo:3:",
|
||||
today_revision=today["revision"], week_revision=week["revision"],
|
||||
)
|
||||
with pytest.raises(TodayPromotionConflict):
|
||||
store.pull_week_item(
|
||||
"timmy", operation_id="stale", identity="issue:secret/repo:3:",
|
||||
today_revision=today["revision"] - 1, week_revision=week["revision"],
|
||||
)
|
||||
|
||||
assert store.get("timmy") == today
|
||||
assert store.get_week("timmy") == week
|
||||
|
||||
|
||||
def test_normal_week_promotion_still_rejects_a_future_day_without_writes(tmp_path):
|
||||
store = TodayStore(
|
||||
tmp_path / "today.sqlite3",
|
||||
@@ -327,6 +375,26 @@ async def test_week_early_start_api_promotes_a_future_day(monkeypatch, tmp_path)
|
||||
assert store.get_week("timmy")["days"] == [sample_days()[1]]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pull_week_item_api_returns_both_atomic_plans(monkeypatch, tmp_path):
|
||||
async def user():
|
||||
return {"login": "timmy"}
|
||||
|
||||
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"u" * 32)
|
||||
today = store.apply("timmy", "seed", "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.pull_week_item_into_today(main.WeekItemPull(
|
||||
operation_id="pull-api", identity="issue:secret/repo:3:",
|
||||
week_revision=week["revision"], today_revision=today["revision"],
|
||||
))
|
||||
|
||||
assert result["today"]["ids"] == ["issue:r:active:", "issue:secret/repo:3:"]
|
||||
assert result["week"]["days"][0]["ids"] == ["issue:secret/repo:2:"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_week_reconciliation_api_returns_atomic_today_and_remaining_week(monkeypatch, tmp_path):
|
||||
async def user():
|
||||
|
||||
@@ -300,6 +300,31 @@ console.log(JSON.stringify({before,refused,moved,added,state:week.state(),pendin
|
||||
assert result["pending"]["days"] == result["state"]["days"]
|
||||
|
||||
|
||||
def test_week_controller_pulls_one_item_into_today_and_adopts_returned_week():
|
||||
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:['active',body.identity],capacity_minutes:120,estimates:{[body.identity]:45}},
|
||||
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');
|
||||
console.log(JSON.stringify({pulled,requests,state:week.state()}));
|
||||
""")
|
||||
|
||||
assert result["requests"] == [{
|
||||
"url": "api/v1/week/pull-item",
|
||||
"body": {
|
||||
"operation_id": "stable-pull", "identity": "pull",
|
||||
"today_revision": 4, "week_revision": 7,
|
||||
},
|
||||
}]
|
||||
assert result["pulled"]["today"]["ids"] == ["active", "pull"]
|
||||
assert result["state"]["revision"] == 8
|
||||
assert result["state"]["days"][0]["ids"] == ["sibling"]
|
||||
|
||||
|
||||
def test_week_controller_retires_completed_work_from_every_day_in_one_durable_transition():
|
||||
result = run_controller("""
|
||||
const values=new Map();let writes=0;
|
||||
@@ -407,28 +432,37 @@ const dateRoot=makeElement();
|
||||
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
|
||||
elements.set('#week-plan-dates',dateRoot);
|
||||
const reviewDays=makeElement();
|
||||
Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;},get(){return this.value||'';}});
|
||||
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());
|
||||
let writes=0,opened=0,reviewMode=false;
|
||||
let writes=0,opened=0,reviewMode=false,pullRequest=null,replacedToday=null;
|
||||
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
|
||||
ids:index===2?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60,
|
||||
estimates:index===2?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===2?30:0,overloaded:false})),
|
||||
duplicates:[],blockers:[],can_confirm:true};
|
||||
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
|
||||
pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};},
|
||||
load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,
|
||||
move:()=>true,flush:async()=>{writes+=1;}};
|
||||
load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>false,
|
||||
move:()=>true,flush:async()=>{writes+=1;},pullItem:async(id,today,operationId)=>{
|
||||
pullRequest={id,today,operationId};return {today:{revision:4,ids:['active',id],estimates:{[id]:30}},week:{revision:8,days:[]}};
|
||||
}};
|
||||
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
|
||||
getItem:id=>id.includes(':42:')?{kind:'issue',title:'Ship mobile overview',repository:'stackchain/dashboard',number:42}:null,
|
||||
openPlanner:()=>{opened+=1;},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
||||
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||
today:()=>({revision:3,ids:['active'],estimates:{}}),operationId:()=> 'pull-once',
|
||||
todayWork:{replace:ids=>{replacedToday=ids;},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||
await workflow.open({disabled:false});
|
||||
const initialMarkup=reviewDays.innerHTML;
|
||||
await reviewDays.pullButtons[0].listeners.click({currentTarget:reviewDays.pullButtons[0]});
|
||||
console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,opened,copy:workflow.copy(),
|
||||
reviewHidden:elements.get('#week-review').hidden,datesHidden:elements.get('#week-plan-dates').hidden,
|
||||
markup:reviewDays.innerHTML,status:elements.get('#week-review-status').textContent,
|
||||
markup:initialMarkup,status:elements.get('#week-review-status').textContent,pullRequest,replacedToday,
|
||||
confirmHidden:elements.get('#confirm-week-plan').hidden,editWeekHidden:elements.get('#edit-week-plan').hidden,
|
||||
editWeekLabel:elements.get('#edit-week-plan').textContent}));
|
||||
""")
|
||||
@@ -444,8 +478,12 @@ console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,ope
|
||||
assert result["datesHidden"] is True
|
||||
assert "Ship mobile overview" in result["markup"]
|
||||
assert "Next up" in result["markup"]
|
||||
assert "Add to Today" in result["markup"]
|
||||
assert result["pullRequest"]["id"] == "issue:stackchain/dashboard:42:"
|
||||
assert result["pullRequest"]["operationId"] == "pull-once"
|
||||
assert result["replacedToday"] == ["active", "issue:stackchain/dashboard:42:"]
|
||||
assert "data-week-move" not in result["markup"]
|
||||
assert result["status"] == "Week Ahead overview · no changes made."
|
||||
assert result["status"] == "Ship mobile overview added to Today."
|
||||
assert result["confirmHidden"] is True
|
||||
assert result["editWeekHidden"] is False
|
||||
assert result["editWeekLabel"] == "Edit week"
|
||||
@@ -661,6 +699,10 @@ def test_dashboard_routes_week_overview_controls_through_existing_detail_flow_wi
|
||||
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
|
||||
assert ".week-review-pull" in css
|
||||
pull_rule = css.split(".week-review-pull", 1)[1].split("}", 1)[0]
|
||||
assert "min-height:44px" in pull_rule
|
||||
assert "width:100%" in pull_rule
|
||||
|
||||
|
||||
def test_successful_issue_close_notifies_week_ahead_only_after_upstream_confirmation():
|
||||
|
||||
Reference in new issue
Block a user