feat: persist exact Week Ahead task times (Closes #1270)
CI / lint (pull_request) Successful in 4m3s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 6m22s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-22 16:39:24 +00:00
1 parent f78da3dfc9
commit 5fb4da30a4
11 files changed
+180 -18

No files matched your search

+10 -1
View File
@@ -328,9 +328,18 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(start).to_have_value("09:00")
expect(page.locator('[data-week-calendar-preview="issue:acme/mobile:41:"]')).to_have_text("10:00–10:45 · 45 min")
expect(page.locator("#week-calendar-status")).to_contain_text("Planning around imported busy time")
for control in (start, page.locator("#back-to-week-review-from-calendar"), page.locator("#share-week-calendar")):
exact = page.locator('[data-week-calendar-exact="issue:acme/mobile:41:"]')
expect(exact).to_have_value("")
exact.fill("14:00")
expect(page.locator('[data-week-calendar-preview="issue:acme/mobile:41:"]')).to_have_text("14:00–14:45 · 45 min")
save_times = page.locator("#save-week-calendar-times")
for control in (start, exact, page.locator("#back-to-week-review-from-calendar"), save_times, page.locator("#share-week-calendar")):
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
save_times.click()
expect(page.locator("#week-calendar-status")).to_have_text("Exact task times saved to Week Ahead.")
assert saved[-1]["days"][0]["start_times"] == {"issue:acme/mobile:41:": "14:00"}
page.locator("#back-to-week-review-from-calendar").click()
expect(page.locator("#week-review")).to_be_visible()
confirm.click()
+25
View File
@@ -6,6 +6,7 @@ from pathlib import Path
WEEK_CALENDAR = Path(__file__).parents[1] / "frontend" / "week-calendar.js"
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
BUNDLE = Path(__file__).parents[1] / "src" / "frontend_bundle.py"
@@ -117,6 +118,26 @@ console.log(JSON.stringify(result));
}]
def test_week_calendar_honors_exact_saved_task_times_and_rejects_conflicts():
source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
const items={{one:{{title:'First'}},two:{{title:'Second'}}}};
const plan={{days:[{{plan_date:'2026-08-21',ids:['one','two'],estimates:{{one:45,two:90}},
free_windows:[{{start_time:'09:00',end_time:'10:00'}},{{start_time:'13:00',end_time:'17:00'}}],
start_times:{{one:'09:00',two:'14:00'}}}}]}};
const saved=calendar.buildWeekSchedule(plan,{{}},id=>items[id]);
const conflict=calendar.buildWeekSchedule(plan,{{one:'14:30',two:'14:00'}},id=>items[id]);
console.log(JSON.stringify({{saved,conflict}}));
"""
result = run_node(source)
assert [(block["id"], block["start_time"], block["end_time"]) for block in result["saved"]["blocks"]] == [
("one", "09:00", "09:45"), ("two", "14:00", "15:30")
]
assert result["conflict"]["blocks"] == []
assert {blocker["id"] for blocker in result["conflict"]["blockers"]} == {"one", "two"}
def test_week_calendar_excludes_unselected_items_and_downloads_when_native_share_is_unavailable():
source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
@@ -150,6 +171,10 @@ def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_co
assert 'id="week-calendar-handoff"' in index
assert 'id="week-calendar-days"' in index
assert 'id="share-week-calendar"' in index
assert 'id="save-week-calendar-times"' in index
assert 'data-week-calendar-exact' in WEEK_CALENDAR.read_text()
assert "onSave" in WEEK_CALENDAR.read_text()
assert 'min-height:44px' in CSS.read_text()[CSS.read_text().index('.week-calendar-time'):]
assert 'id="back-to-week-review-from-calendar"' in index
assert 'id="export-saved-week-calendar"' in index
assert "Add saved week to calendar" in index
+24
View File
@@ -101,6 +101,30 @@ def test_week_plan_persists_only_bounded_non_overlapping_calendar_free_windows(t
)
def test_week_plan_persists_only_valid_exact_task_start_times(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"w" * 32)
days = sample_days()
days[0]["start_times"] = {
"issue:secret/repo:3:": "09:00",
"issue:secret/repo:2:": "10:15",
}
saved = store.replace_week(
"timmy", base_revision=0, days=days, timezone="UTC",
)
assert saved["days"][0]["start_times"] == days[0]["start_times"]
assert store.get_week("timmy") == saved
assert b"09:00" not in (tmp_path / "today.sqlite3").read_bytes()
invalid = sample_days()
invalid[0]["start_times"] = {"issue:secret/repo:3:": "23:30"}
with pytest.raises(ValueError, match="task start times"):
store.replace_week(
"timmy", base_revision=1, days=invalid, timezone="UTC",
)
def test_week_update_model_accepts_zero_capacity_weekday_defaults():
payload = main.WeekPlanUpdate.model_validate({
"base_revision": 2, "timezone": "UTC", "days": [],
+23
View File
@@ -276,6 +276,29 @@ console.log(JSON.stringify({pending,request:requests[0],restored:restored.state(
assert result["restored"]["days"][0]["free_windows"] == windows
def test_week_controller_keeps_exact_task_times_through_pending_flush_and_reload():
result = run_controller("""
const values=new Map();const requests=[];let remote={revision:4,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:420,
estimates:{one:45,two:90},start_times:{one:'09:00'}}]};
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const fetchJson=async(url,options={})=>{
if(!options.method)return remote;
const body=JSON.parse(options.body);requests.push(body);remote={revision:5,timezone:body.timezone,days:body.days};return remote;
};
const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);await first.load();
first.stageStartTimes('2026-08-21',{one:'09:00',two:'11:00'});
const pending=first.pending();await first.flush();
const restored=createWeekPlan(options);await restored.load();
console.log(JSON.stringify({pending,request:requests[0],restored:restored.state()}));
""")
expected = {"one": "09:00", "two": "11:00"}
assert result["pending"]["days"][0]["start_times"] == expected
assert result["request"]["days"][0]["start_times"] == expected
assert result["restored"]["days"][0]["start_times"] == expected
def test_week_controller_previews_and_atomically_applies_a_capacity_safe_reflow():
result = run_controller("""
const values=new Map();let writes=0;