feat: persist calendar free windows with Week Ahead (Closes #1258)
CI / lint (pull_request) Successful in 3m40s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m58s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-22 09:49:04 +00:00
1 parent 15739f7370
commit fc93620118
10 files changed
+178 -7

No files matched your search

@@ -285,15 +285,20 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
page.locator("#week-capacity-file"),
page.locator("#week-capacity-start"),
page.locator("#week-capacity-end"),
page.locator("#keep-week-free-times"),
page.locator("#apply-week-capacities"),
):
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#keep-week-free-times").check()
page.locator("#apply-week-capacities").click()
expect(capacity_import).to_be_hidden()
expect(page.locator("#week-review")).to_be_visible()
expect(first_day).to_contain_text("45 of 420 min")
assert saved[-1]["days"][0]["free_windows"] == [
{"start_time": "10:00", "end_time": "17:00"}
]
page.wait_for_function("() => !document.querySelector('#confirm-week-plan').disabled")
confirm = page.locator("#confirm-week-plan")
+32
View File
@@ -303,6 +303,37 @@ console.log(JSON.stringify({reviewed,staged,flushes,appliedCalls,rootHidden:elem
assert "Do not retain me" not in json.dumps(result)
def test_calendar_import_persists_only_free_windows_with_explicit_consent_and_restores_them():
result = run_import("""
const elements=new Map();
const element=(value='')=>({value,checked:false,hidden:false,textContent:'',innerHTML:'',disabled:false,files:[],listeners:{},
addEventListener(name,listener){this.listeners[name]=listener;},focus(){}});
for(const selector of ['#week-capacity-import','#week-capacity-review','#week-capacity-days','#week-capacity-status',
'#week-capacity-file','#week-capacity-start','#week-capacity-end','#keep-week-free-times',
'#apply-week-capacities','#cancel-week-capacity-import']) elements.set(selector,element());
elements.get('#week-capacity-start').value='09:00';elements.get('#week-capacity-end').value='17:00';
elements.get('#keep-week-free-times').checked=true;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
let staged=null;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+index})),state:()=>({days:staged||[]}),
stageCapacities:value=>{staged=value;return {sync_pending:true};},flush:async()=>({})};
const workflow=calendarImport.createWorkflow({controller,qs:selector=>elements.get(selector)});
workflow.review(`BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Secret meeting\r\nATTENDEE:private@example.com\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nEND:VEVENT\r\nEND:VCALENDAR`);
await workflow.apply();
const restored=calendarImport.createWorkflow({controller,qs:selector=>elements.get(selector)}).availability();
console.log(JSON.stringify({staged,restored}));
""")
windows = [
{"start_time": "09:00", "end_time": "10:00"},
{"start_time": "11:00", "end_time": "17:00"},
]
assert result["staged"][0]["free_windows"] == windows
assert result["restored"][0]["free_windows"] == windows
assert "Secret meeting" not in json.dumps(result)
assert "private@example.com" not in json.dumps(result)
def test_week_ahead_exposes_private_calendar_capacity_import_in_the_packaged_planning_flow():
html = (FRONTEND / "index.html").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
@@ -313,6 +344,7 @@ def test_week_ahead_exposes_private_calendar_capacity_import_in_the_packaged_pla
assert 'id="week-capacity-file"' in html and 'accept=".ics,text/calendar"' in html
assert 'id="week-capacity-start"' in html and 'id="week-capacity-end"' in html
assert 'id="week-capacity-review"' in html
assert 'id="keep-week-free-times"' in html
assert 'id="apply-week-capacities"' in html
assert '<script src="static/week-calendar-import.js"></script>' in html
assert html.index('static/week-calendar-import.js') < html.index('static/week-plan.js')
+51
View File
@@ -73,6 +73,34 @@ def test_week_plan_persists_bounded_weekly_availability_with_the_revisioned_plan
)
def test_week_plan_persists_only_bounded_non_overlapping_calendar_free_windows(tmp_path):
path = tmp_path / "today.sqlite3"
store = TodayStore(path, encryption_key=b"w" * 32)
days = sample_days()
days[0]["free_windows"] = [
{"start_time": "09:00", "end_time": "10:00"},
{"start_time": "11:00", "end_time": "17:00"},
]
saved = store.replace_week(
"timmy", base_revision=0, days=days, timezone="UTC",
)
assert saved["days"][0]["free_windows"] == days[0]["free_windows"]
assert store.get_week("timmy") == saved
assert b"09:00" not in path.read_bytes()
invalid = sample_days()
invalid[0]["free_windows"] = [
{"start_time": "09:00", "end_time": "12:00"},
{"start_time": "11:00", "end_time": "13:00"},
]
with pytest.raises(ValueError, match="free windows"):
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": [],
@@ -87,6 +115,29 @@ def test_week_update_model_accepts_zero_capacity_weekday_defaults():
})
def test_week_update_model_accepts_only_bounded_calendar_free_windows():
payload = main.WeekPlanUpdate.model_validate({
"base_revision": 2, "timezone": "UTC", "days": [{
"plan_date": "2026-08-21", "ids": [], "estimates": {},
"free_windows": [
{"start_time": "09:00", "end_time": "10:00"},
{"start_time": "11:00", "end_time": "17:00"},
],
}],
})
assert payload.model_dump()["days"][0]["free_windows"][1] == {
"start_time": "11:00", "end_time": "17:00",
}
with pytest.raises(ValueError):
main.WeekPlanUpdate.model_validate({
"base_revision": 2, "timezone": "UTC", "days": [{
"plan_date": "2026-08-21", "ids": [], "estimates": {},
"free_windows": [{"start_time": "nine", "end_time": "10:00"}],
}],
})
def test_start_day_plan_selects_earliest_nonempty_week_date_without_mutation(tmp_path):
store = TodayStore(
tmp_path / "today.sqlite3", encryption_key=b"r" * 32,
+27
View File
@@ -249,6 +249,33 @@ console.log(JSON.stringify({applied,writes,state:week.state(),pending:week.pendi
assert result["pending"]["days"] == result["state"]["days"]
def test_week_controller_keeps_consented_free_windows_through_pending_flush_and_reload():
result = run_controller("""
const values=new Map();const requests=[];
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 {revision:4,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:420,
estimates:{one:45},free_windows:[{start_time:'09:00',end_time:'10:00'},{start_time:'11:00',end_time:'17:00'}]}]};
const body=JSON.parse(options.body);requests.push(body);return {revision:5,timezone:body.timezone,days:body.days};
};
const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);await first.load();
const capacities=first.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:index?480:420,
...(index?{}:{free_windows:[{start_time:'09:00',end_time:'10:00'},{start_time:'11:00',end_time:'17:00'}]})}));
first.stageCapacities(capacities);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()}));
""")
windows = [
{"start_time": "09:00", "end_time": "10:00"},
{"start_time": "11:00", "end_time": "17:00"},
]
assert result["pending"]["days"][0]["free_windows"] == windows
assert result["request"]["days"][0]["free_windows"] == windows
assert result["restored"]["days"][0]["free_windows"] == windows
def test_week_controller_previews_and_atomically_applies_a_capacity_safe_reflow():
result = run_controller("""
const values=new Map();let writes=0;