feat: persist exact Week Ahead task times (Closes #1270)
This commit is contained in:
1 parent
f78da3dfc9
commit
5fb4da30a4
11 files changed
+180
-18
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user