feat: reconcile long Today interruptions (Closes #591)
CI / lint (pull_request) Successful in 1m26s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-11 20:56:40 +00:00
1 parent 10a8b2738b
commit aaa9aef78b
5 files changed
+298 -1

No files matched your search

+172
View File
@@ -1976,6 +1976,159 @@ process.stdout.write(JSON.stringify({{
}
def test_today_timer_reconciles_long_away_time_once_and_keeps_short_switches_seamless():
script = f"""
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
const values = new Map();
const storage = {{
getItem:key => values.has(key) ? values.get(key) : null,
setItem:(key,value) => values.set(key,value),
removeItem:key => values.delete(key),
}};
let login = 'timmy';
let now = 0;
const timer = createTodayTimer({{storage,getLogin:() => login,now:() => now}});
timer.activate('issue:repo:591:');
now = 60000;
timer.markAway();
now = 4 * 60000;
const short = timer.reconcileInterruption();
now = 5 * 60000;
timer.markAway();
now = 11 * 60000;
const pending = timer.reconcileInterruption();
now = 12 * 60000;
const frozen = timer.snapshot();
const restored = createTodayTimer({{storage,getLogin:() => login,now:() => now}}).pendingInterruption();
const counted = timer.resolveInterruption('count');
const afterCount = timer.snapshot();
const duplicate = timer.resolveInterruption('count');
now = 13 * 60000;
timer.markAway();
now = 19 * 60000;
timer.reconcileInterruption();
const excluded = timer.resolveInterruption('exclude');
const afterExclude = timer.snapshot();
login = 'alexander';
const isolated = timer.pendingInterruption();
process.stdout.write(JSON.stringify({{
short,pending,frozen,restored,counted,afterCount,duplicate,excluded,afterExclude,isolated,
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"short": None,
"pending": {
"identity": "issue:repo:591:",
"away_ms": 360000,
},
"frozen": {
"identity": "issue:repo:591:",
"elapsed_ms": 660000,
"running": False,
},
"restored": {
"identity": "issue:repo:591:",
"away_ms": 360000,
},
"counted": True,
"afterCount": {
"identity": "issue:repo:591:",
"elapsed_ms": 660000,
"running": True,
},
"duplicate": False,
"excluded": True,
"afterExclude": {
"identity": "issue:repo:591:",
"elapsed_ms": 720000,
"running": True,
},
"isolated": None,
}
def test_today_interruption_prompt_restores_and_resolves_the_pending_mobile_decision():
script = f"""
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
const values = new Map();
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
let now = 0;
const timer = createTodayTimer({{storage,getLogin:()=>'timmy',now:()=>now}});
const sheet = {{hidden:true}};
const description = {{textContent:''}};
let resolved = 0;
const prompt = createTodayTimer.createInterruptionPrompt({{
timer, sheet, description,
getItemLabel: identity => identity === 'issue:r:7:' ? 'Fix mobile timer' : '',
onResolved: () => resolved++,
}});
timer.activate('issue:r:7:');
now = 60000;
prompt.background();
now = 7 * 60000;
const opened = prompt.foreground();
const shown = {{hidden:sheet.hidden, text:description.textContent}};
const restored = prompt.restore();
const excluded = prompt.resolve('exclude');
const closed = sheet.hidden;
process.stdout.write(JSON.stringify({{opened,shown,restored,excluded,closed,resolved}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"opened": True,
"shown": {
"hidden": False,
"text": "Fix mobile timer · away for 6 minutes",
},
"restored": True,
"excluded": True,
"closed": True,
"resolved": 1,
}
def test_today_interruption_detection_is_idempotent_and_fails_closed_for_invalid_state():
script = f"""
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
const values = new Map();
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
let now = 0;
const timer = createTodayTimer({{storage,getLogin:()=>'timmy',now:()=>now}});
timer.activate('issue:r:1:');
now = 60000;
timer.markAway();
now = 120000;
timer.markAway();
now = 6 * 60000;
const originalCheckpoint = timer.reconcileInterruption();
timer.resolveInterruption('exclude');
timer.pause();
now = 20 * 60000;
const paused = timer.markAway();
const key = 'stackchain.today-timer.v1.timmy';
const invalid = JSON.parse(values.get(key));
invalid.pending_interruption = {{identity:7,away_ms:'bad'}};
invalid.away_at = now + 1000;
values.set(key, JSON.stringify(invalid));
const malformed = timer.reconcileInterruption();
process.stdout.write(JSON.stringify({{originalCheckpoint,paused,malformed,pending:timer.pendingInterruption()}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"originalCheckpoint": {"identity": "issue:r:1:", "away_ms": 300000},
"paused": False,
"malformed": None,
"pending": None,
}
def test_today_timer_switches_items_retains_elapsed_time_and_is_account_bound():
script = f"""
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
@@ -2057,6 +2210,25 @@ async def test_today_timer_is_wired_into_every_mobile_session_control():
assert '.work-session-nav [data-work-session-timer-toggle] { min-height:44px;' in html
@pytest.mark.anyio
async def test_dashboard_reconciles_long_today_interruptions_in_a_touch_safe_sheet():
html = await dashboard()
assert 'id="today-interruption-sheet"' in html
assert 'id="today-interruption-description"' in html
assert 'data-today-interruption="count"' in html
assert 'data-today-interruption="exclude"' in html
assert "const interruptionPrompt = createTodayInterruptionPrompt({" in html
assert "if (document.hidden) interruptionPrompt.background();" in html
assert "else interruptionPrompt.foreground();" in html
assert "window.addEventListener('pagehide', () => interruptionPrompt.background());" in html
assert "interruptionPrompt.restore();" in html
assert "interruptionPrompt.resolve(button.dataset.todayInterruption)" in html
assert ".today-interruption-panel { box-sizing:border-box; width:min(620px,100%);" in html
assert "padding-bottom:calc(18px + env(safe-area-inset-bottom))" in html
assert ".today-interruption-actions button { min-height:44px; width:100%; }" in html
def test_active_today_session_reopens_current_item_without_restarting_checkpoint():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});