feat: pause Today during quick capture (Closes #1076)
CI / lint (pull_request) Successful in 3m23s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 2m35s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 11:06:44 +00:00
1 parent d9749e3a6f
commit f8c2541d30
7 files changed
+304 -7

No files matched your search

@@ -94,6 +94,32 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
if page.locator("#plan-today-sheet").is_visible():
page.locator("#cancel-plan-today").click()
expect(page.locator("[data-mobile-today-hud]")).to_be_visible()
page.locator('[data-mobile-task="new"]').click()
expect(page.locator("#create-issue-sheet")).to_have_class("create-issue-sheet open")
expect(page.locator("#today-capture-interruption")).to_be_visible()
expect(page.locator("#today-capture-interruption-label")).to_contain_text("Ship mobile capture")
expect(page.locator("#save-unfiled-issue")).to_have_text("Save & return to Today")
paused_timer = page.evaluate("""() => {
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
const state = JSON.parse(localStorage.getItem(key));
return state.entries[state.active_identity];
}""")
assert paused_timer["running"] is False
page.locator("#create-issue-title").fill("Remember release note")
page.locator("#save-unfiled-issue").click()
expect(page.locator("#create-issue-sheet")).not_to_have_class("create-issue-sheet open")
expect(page.locator("[data-mobile-today-hud]")).to_be_visible()
resumed = page.evaluate("""() => {
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
const timer = JSON.parse(localStorage.getItem(key));
const captures = JSON.parse(localStorage.getItem('stackchain.unfiled-issues.v1') || 'null');
return {running:timer.entries[timer.active_identity].running,
saved:JSON.stringify(captures).includes('Remember release note')};
}""")
assert resumed == {"running": True, "saved": True}
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("[data-mobile-today-more]").click()
expect(page.locator("#mobile-today-actions")).to_be_visible()
blocked = page.locator("[data-mobile-today-blocked]")
+30
View File
@@ -0,0 +1,30 @@
from pathlib import Path
ROOT = Path(__file__).parents[1]
DASHBOARD = ROOT / "frontend" / "dashboard.js"
INDEX = ROOT / "frontend" / "index.html"
TIMER = ROOT / "frontend" / "today-timer.js"
def test_quick_capture_wires_timer_interruption_to_all_exit_paths():
source = DASHBOARD.read_text()
timer_source = TIMER.read_text()
html = INDEX.read_text()
assert 'id="today-capture-interruption"' in html
assert 'id="today-capture-interruption-label"' in html
assert "view.beginCapture();" in timer_source
assert "timerView.transferCapture()" in source
assert "timerView.finishCapture()" in source
assert "result ? 'Save & return to Today' : 'Save to Drafts'" in timer_source
assert "if (!capture)" in source
def test_capture_save_returns_only_after_durable_admission():
source = DASHBOARD.read_text()
saved = source.index("const savedCapture = await unfiledCaptures.save(captureDraft);")
returned = source.index("timerView.finishCapture()", saved)
failure = source.index("} catch (error) {", saved)
assert saved < returned < failure
+124
View File
@@ -49,3 +49,127 @@ process.stdout.write(JSON.stringify(changes));
{"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},
}