feat: pause Today during mobile Insights (Closes #1082)
CI / lint (pull_request) Successful in 3m18s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 2m45s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 14:44:08 +00:00
1 parent d7b8cd5e71
commit 472371d5a8
8 files changed
+168 -19

No files matched your search

+27 -2
View File
@@ -351,7 +351,7 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
fake_thread.join(timeout=5)
def test_release_artifact_pauses_today_across_find_and_queue_detours(tmp_path: Path):
def test_release_artifact_pauses_today_across_mobile_work_and_insights_detours(tmp_path: Path):
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
assert len(archives) == 1
fake = FakeGiteaServer(("127.0.0.1", 0))
@@ -381,7 +381,7 @@ def test_release_artifact_pauses_today_across_find_and_queue_detours(tmp_path: P
missing.nth(0).press("Tab")
page.locator("#save-and-start-today").click()
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
page.locator("#close-issue-sheet").click()
page.locator("#close-issue-sheet").click(force=True)
if page.locator("#plan-today-sheet").is_visible():
page.locator("#cancel-plan-today").click()
@@ -421,6 +421,31 @@ def test_release_artifact_pauses_today_across_find_and_queue_detours(tmp_path: P
const timer = JSON.parse(localStorage.getItem(key));
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
}""")
page.locator("#close-issue-sheet").click()
page.locator("#app-menu-toggle").click()
page.locator("#open-insights").click()
insights_pause = page.locator("#insights-sheet > [data-today-detour]")
expect(insights_pause).to_be_visible()
expect(insights_pause).to_contain_text("Today paused · Ship mobile capture")
bounds = insights_pause.locator("[data-return-from-detour]").bounding_box()
assert bounds and bounds["height"] >= 44
assert page.evaluate("""() => {
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
const timer = JSON.parse(localStorage.getItem(key));
return timer.entries[timer.active_identity].running === false && timer.detour_interruption?.reason === 'insights';
}""")
page.reload(wait_until="networkidle")
restored_pause = page.locator("#insights-sheet > [data-today-detour]")
expect(restored_pause).to_be_visible()
expect(restored_pause).to_contain_text("Today paused · Ship mobile capture")
restored_pause.locator("[data-return-from-detour]").click()
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
assert page.evaluate("""() => {
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
const timer = JSON.parse(localStorage.getItem(key));
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
}""")
browser.close()
finally:
fake.shutdown()
+69
View File
@@ -150,6 +150,70 @@ process.stdout.write(JSON.stringify({{backs:history.backs,prevented}}));
assert run_node(script) == {"backs": 1, "prevented": 1}
def test_insights_route_owns_the_today_detour_for_open_back_and_reload():
script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const listeners={{}};
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/today'}};
const calls=[];
const detour={{beginDetour(reason){{calls.push('begin:'+reason);return {{identity:'issue:r:42:',reason}};}},finishDetour(){{calls.push('finish');return {{identity:'issue:r:42:'}};}}}};
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start();
launcher.listeners.click();
location.hash='#/my-work/today'; history.state=null; listeners.popstate();
listeners.popstate();
location.hash='#/insights'; listeners.hashchange();
process.stdout.write(JSON.stringify({{calls,current:controller.current()}}));
"""
assert run_node(script) == {
"calls": ["begin:insights", "finish", "begin:insights"],
"current": True,
}
def test_return_to_today_closes_insights_and_finishes_the_detour_once():
script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/today'}};
const calls=[];
const detour={{beginDetour(reason){{calls.push('begin:'+reason);}},finishDetour(){{calls.push('finish');}}}};
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(){{}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start(); launcher.listeners.click(); returnButton.listeners.click();
process.stdout.write(JSON.stringify({{calls,current:controller.current(),hash:location.hash}}));
"""
assert run_node(script) == {
"calls": ["begin:insights", "finish"],
"current": False,
"hash": "#/my-work/today",
}
def test_direct_insights_route_retries_detour_after_identity_restores():
script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/insights'}};
let attempts=0; let retry;
global.setTimeout=callback=>{{retry=callback;}};
const detour={{beginDetour(){{attempts+=1;return attempts > 1 ? {{identity:'issue:r:42:'}} : null;}},finishDetour(){{}}}};
const history={{state:null,replaceState(){{}},pushState(){{}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(){{}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start(); retry();
process.stdout.write(JSON.stringify({{attempts,current:controller.current()}}));
"""
assert run_node(script) == {"attempts": 2, "current": True}
@pytest.mark.anyio
async def test_mobile_home_progressively_discloses_secondary_panels_as_insights():
html = await dashboard()
@@ -157,6 +221,11 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
assert 'id="open-insights"' in html
assert 'id="insights-sheet"' in html
assert 'id="close-insights"' in html
insights = html[html.index('id="insights-sheet"'):html.index('</section>', html.index('id="insights-sheet"'))]
assert 'data-today-detour' in insights
assert 'data-return-from-detour' in insights
assert "detour:() => timerView" in html
assert html.index("const timerView = createTodayTimerView({") < html.index("mobileInsights.start();")
assert html.index('id="my-work"') < html.index('id="insights-sheet"')
assert html.index('id="insights-sheet"') < html.index('data-panel-key="context"')
assert html.index('data-panel-key="markdown"') < html.index('</section>\n</main>')
+2 -2
View File
@@ -254,8 +254,8 @@ process.stdout.write(JSON.stringify({{calls, open:queues.open}}));
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
html = await dashboard()
assert html.count('data-today-detour role="status"') == 3
assert html.count('data-return-from-detour type="button"') == 3
assert html.count('data-today-detour role="status"') == 4
assert html.count('data-return-from-detour type="button"') == 4
assert "detour:() => timerView" in html
timer_source = TIMER.read_text()
assert "createTodayDetourInterruption" in timer_source
+29
View File
@@ -260,6 +260,35 @@ process.stdout.write(JSON.stringify({interruption, paused, durable, returned, re
}
def test_insights_detour_excludes_review_time_and_survives_reload():
script = SOURCE.read_text() + r"""
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
let now = 1000;
let timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now});
timer.activate('issue:r:42:');
now = 6000;
const interruption = timer.beginDetour('insights');
now = 26000;
timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now});
const restored = timer.detourInterruption();
const paused = timer.snapshot();
const returned = timer.returnFromDetour();
now = 28000;
process.stdout.write(JSON.stringify({interruption, restored, paused, returned, resumed:timer.snapshot()}));
"""
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert completed.returncode == 0, completed.stderr
assert json.loads(completed.stdout) == {
"interruption": {"identity": "issue:r:42:", "resume": True, "reason": "insights"},
"restored": {"identity": "issue:r:42:", "resume": True, "reason": "insights"},
"paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False},
"returned": {"identity": "issue:r:42:", "resumed": True, "reason": "insights"},
"resumed": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True},
}
def test_mobile_detour_never_resumes_manually_paused_or_replaced_work():
script = SOURCE.read_text() + r"""
const values = new Map();