import json import subprocess from pathlib import Path SOURCE = Path(__file__).parents[1] / "frontend" / "today-timer.js" def test_timer_adopts_claimed_elapsed_time(): 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 = 100000; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); const adopted = timer.adopt('issue:r:42:', 90000, true); now = 105000; process.stdout.write(JSON.stringify({adopted, snapshot:timer.snapshot()})); """ completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) result = json.loads(completed.stdout) assert result == { "adopted": True, "snapshot": {"identity": "issue:r:42:", "elapsed_ms": 95000, "running": True}, } def test_timer_reports_durable_changes_for_session_sync(): 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 = 0; const changes = []; const timer = createTodayTimer({ storage, getLogin:()=> 'timmy', now:()=>now, onChange:snapshot=>changes.push(snapshot), }); timer.activate('issue:r:42:'); now = 5000; timer.pause(); process.stdout.write(JSON.stringify(changes)); """ completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) assert json.loads(completed.stdout) == [ {"identity": "issue:r:42:", "elapsed_ms": 0, "running": True}, {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False}, ] def test_quick_capture_pauses_only_its_running_item_and_restores_once(): 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; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); timer.activate('issue:r:42:'); now = 6000; const interruption = timer.beginCapture(); now = 16000; const paused = timer.snapshot(); const returned = timer.returnFromCapture(); now = 18000; const restored = timer.snapshot(); const repeated = timer.returnFromCapture(); process.stdout.write(JSON.stringify({interruption, paused, returned, restored, repeated})); """ 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}, "paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False}, "returned": {"identity": "issue:r:42:", "resumed": True}, "restored": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True}, "repeated": None, } def test_quick_capture_does_not_resume_an_already_paused_or_replaced_item(): 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 = 0; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); timer.adopt('issue:r:1:', 3000, false); const pausedInterruption = timer.beginCapture(); const pausedReturn = timer.returnFromCapture(); const stayedPaused = timer.snapshot(); timer.activate('issue:r:1:'); const runningInterruption = timer.beginCapture(); timer.activate('issue:r:2:'); const replacedReturn = timer.returnFromCapture(); const replacement = timer.snapshot(); process.stdout.write(JSON.stringify({pausedInterruption, pausedReturn, stayedPaused, runningInterruption, replacedReturn, replacement})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "pausedInterruption": {"identity": "issue:r:1:", "resume": False}, "pausedReturn": {"identity": "issue:r:1:", "resumed": False}, "stayedPaused": {"identity": "issue:r:1:", "elapsed_ms": 3000, "running": False}, "runningInterruption": {"identity": "issue:r:1:", "resume": True}, "replacedReturn": {"identity": "issue:r:1:", "resumed": False}, "replacement": {"identity": "issue:r:2:", "elapsed_ms": 0, "running": True}, } def test_create_and_start_abandons_the_interrupted_timer(): script = SOURCE.read_text() + r""" const values = new Map(); const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>5000}); timer.activate('issue:r:1:'); timer.beginCapture(); const abandoned = timer.abandonCapture(); const snapshot = timer.snapshot(); const repeated = timer.returnFromCapture(); process.stdout.write(JSON.stringify({abandoned, snapshot, repeated})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "abandoned": {"identity": "issue:r:1:", "resumed": False}, "snapshot": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": False}, "repeated": None, } def test_capture_interruption_view_names_work_and_returns_to_today(): script = SOURCE.read_text() + r""" const values = new Map(); const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>1000}); const banner = {hidden:true}; const label = {textContent:''}; const returns = []; const view = createTodayTimer.createCaptureInterruption({ timer, banner, label, getItemLabel:identity=>identity === 'issue:r:1:' ? 'Ship release' : '', onReturn:result=>returns.push(result), }); timer.activate('issue:r:1:'); const opened = view.open(); const shown = {hidden:banner.hidden, text:label.textContent, timer:timer.snapshot()}; const finished = view.finish(); const closed = {hidden:banner.hidden, timer:timer.snapshot(), returns}; timer.beginCapture(); const transferred = view.transfer(); process.stdout.write(JSON.stringify({opened, shown, finished, closed, transferred, final:timer.snapshot()})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "opened": {"identity": "issue:r:1:", "resume": True}, "shown": { "hidden": False, "text": "Today paused · Ship release", "timer": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": False}, }, "finished": {"identity": "issue:r:1:", "resumed": True}, "closed": { "hidden": True, "timer": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": True}, "returns": [{"identity": "issue:r:1:", "resumed": True}], }, "transferred": {"identity": "issue:r:1:", "resumed": False}, "final": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": False}, } def test_search_leaves_manually_paused_work_alone_and_cannot_restore_replaced_work(): 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; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); timer.adopt('issue:r:1:', 3000, false); const pausedSearch = timer.beginSearch(); timer.activate('issue:r:1:'); const runningSearch = timer.beginSearch(); timer.activate('issue:r:2:'); const replacedReturn = timer.returnFromSearch(); process.stdout.write(JSON.stringify({pausedSearch, runningSearch, replacedReturn, snapshot:timer.snapshot()})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "pausedSearch": None, "runningSearch": {"identity": "issue:r:1:", "resume": True}, "replacedReturn": None, "snapshot": {"identity": "issue:r:2:", "elapsed_ms": 0, "running": True}, } def test_search_pauses_running_today_time_and_restores_exactly_once(): 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; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); timer.activate('issue:r:42:'); now = 6000; const interruption = timer.beginSearch(); now = 26000; const paused = timer.snapshot(); const returned = timer.returnFromSearch(); now = 28000; const restored = timer.snapshot(); const repeated = timer.returnFromSearch(); process.stdout.write(JSON.stringify({interruption, paused, returned, restored, repeated})); """ 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}, "paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False}, "returned": {"identity": "issue:r:42:", "resumed": True}, "restored": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True}, "repeated": None, } def test_mobile_detour_pauses_running_today_time_and_restores_exactly_once(): 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; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); timer.activate('issue:r:42:'); now = 6000; const interruption = timer.beginDetour('find'); now = 26000; const paused = timer.snapshot(); const durable = timer.detourInterruption(); const returned = timer.returnFromDetour(); now = 28000; const restored = timer.snapshot(); const repeated = timer.returnFromDetour(); process.stdout.write(JSON.stringify({interruption, paused, durable, returned, restored, repeated})); """ 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": "find"}, "paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False}, "durable": {"identity": "issue:r:42:", "resume": True, "reason": "find"}, "returned": {"identity": "issue:r:42:", "resumed": True, "reason": "find"}, "restored": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True}, "repeated": None, } 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(); const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>1000}); timer.adopt('issue:r:1:', 3000, false); const pausedDetour = timer.beginDetour('queues'); timer.activate('issue:r:1:'); const runningDetour = timer.beginDetour('queues'); timer.activate('issue:r:2:'); const replacedReturn = timer.returnFromDetour(); process.stdout.write(JSON.stringify({pausedDetour, runningDetour, replacedReturn, snapshot:timer.snapshot()})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "pausedDetour": None, "runningDetour": {"identity": "issue:r:1:", "resume": True, "reason": "queues"}, "replacedReturn": None, "snapshot": {"identity": "issue:r:2:", "elapsed_ms": 0, "running": True}, } def test_security_center_close_cannot_finish_an_unrelated_detour(): script = SOURCE.read_text() + r""" const values = new Map(); const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>1000}); timer.activate('issue:r:1:'); timer.beginDetour('insights'); const returned = timer.returnFromDetour('security-center'); process.stdout.write(JSON.stringify({returned, pending:timer.detourInterruption(), snapshot:timer.snapshot()})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "returned": None, "pending": {"identity": "issue:r:1:", "resume": True, "reason": "insights"}, "snapshot": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": False}, } def test_device_setup_detour_excludes_permission_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('device-setup'); 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": "device-setup"}, "restored": {"identity": "issue:r:42:", "resume": True, "reason": "device-setup"}, "paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False}, "returned": {"identity": "issue:r:42:", "resumed": True, "reason": "device-setup"}, "resumed": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True}, } def test_security_center_detour_excludes_investigation_time_and_resumes_once(): 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; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now}); timer.activate('issue:r:42:'); now = 6000; const interruption = timer.beginDetour('security-center'); now = 26000; const paused = timer.snapshot(); const returned = timer.returnFromDetour(); now = 28000; const resumed = timer.snapshot(); const repeated = timer.returnFromDetour(); process.stdout.write(JSON.stringify({interruption, paused, returned, resumed, repeated})); """ 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": "security-center"}, "paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False}, "returned": {"identity": "issue:r:42:", "resumed": True, "reason": "security-center"}, "resumed": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True}, "repeated": None, } def test_mobile_detour_view_names_work_and_returns_to_today(): script = SOURCE.read_text() + r""" const values = new Map(); const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}; const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>1000}); const banner = {hidden:true}; const label = {textContent:''}; const button = {listeners:{}, addEventListener(name, callback) { this.listeners[name] = callback; }, click() { this.listeners.click(); }}; const returns = []; const view = createTodayTimer.createDetourInterruption({ timer, queryAll:selector => ({ '[data-today-detour]':[banner], '[data-today-detour-label]':[label], '[data-return-from-detour]':[button], }[selector] || []), getItemLabel:identity=>identity === 'issue:r:1:' ? 'Ship release' : '', onReturn:identity=>returns.push(identity), }); timer.activate('issue:r:1:'); const opened = view.open('find'); const shown = {hidden:banner.hidden, text:label.textContent, timer:timer.snapshot()}; button.click(); process.stdout.write(JSON.stringify({opened, shown, closed:{hidden:banner.hidden, timer:timer.snapshot(), returns}})); """ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr assert json.loads(completed.stdout) == { "opened": {"identity": "issue:r:1:", "resume": True, "reason": "find"}, "shown": { "hidden": False, "text": "Today paused · Ship release", "timer": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": False}, }, "closed": { "hidden": True, "timer": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": True}, "returns": ["issue:r:1:"], }, }