feat: track actual time through Today (Closes #577)
CI / lint (pull_request) Successful in 1m27s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-11 16:14:17 +00:00
1 parent a8da5ce949
commit 7ee10619a4
11 files changed
+288 -10

No files matched your search

+8 -2
View File
@@ -25,7 +25,8 @@ def test_page_runtime_is_one_deterministic_content_addressed_bundle(tmp_path):
assert first.runtime_gzip_bytes == second.runtime_gzip_bytes
assert gzip.decompress(first.runtime_gzip_bytes) == first.runtime_bytes
assert PAGE_SCRIPT.findall(first.dashboard_html) == []
assert first.dashboard_html.count("<script src=") == 1
assert first.dashboard_html.count("<script src=") == 2
assert f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>' in first.dashboard_html
assert f'<script src="{first.runtime_name}"></script>' in first.dashboard_html
assert first.runtime_name.startswith("runtime-")
assert first.runtime_name.endswith(".js")
@@ -46,8 +47,13 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
first = build_frontend(FRONTEND)
assert set(first.feature_bundles) == {
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup"
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup",
"today-timer",
}
assert (
f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>\n'
f'<script src="{first.runtime_name}"></script>'
) in first.dashboard_html
capture = first.feature_bundles["issue-capture"]
pull_workflow = first.feature_bundles["pull-workflow"]
assert b"function createIssueCapture" not in first.runtime_bytes
+125
View File
@@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
TODAY_TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.js"
LATER_WORK = Path(__file__).parents[1] / "frontend" / "later-work.js"
DETAIL_DEFER = Path(__file__).parents[1] / "frontend" / "detail-defer.js"
LATER_PICKER = Path(__file__).parents[1] / "frontend" / "later-picker.js"
@@ -1931,6 +1932,130 @@ process.stdout.write(JSON.stringify({{finished,active:session.active()}}));
assert json.loads(result.stdout) == {"finished": 1, "active": False}
def test_today_timer_restores_wall_clock_time_once_and_excludes_pauses():
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 now = 1000;
const create = () => createTodayTimer({{
storage, getLogin:() => 'Timmy', now:() => now,
}});
const first = create();
first.activate('issue:stackchain/dashboard:577:');
now += 10 * 60 * 1000;
const beforeReload = first.snapshot();
const restored = create();
const afterReload = restored.snapshot();
restored.pause();
now += 5 * 60 * 1000;
const whilePaused = restored.snapshot();
restored.resume();
now += 2 * 60 * 1000;
const afterResume = restored.snapshot();
process.stdout.write(JSON.stringify({{
api:typeof createTodayTimer,
beforeReload, afterReload, whilePaused, afterResume,
}}));
"""
result = subprocess.run(
["node", "-e", script], capture_output=True, text=True
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"api": "function",
"beforeReload": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 600000, "running": True},
"afterReload": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 600000, "running": True},
"whilePaused": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 600000, "running": False},
"afterResume": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 720000, "running": True},
}
def test_today_timer_switches_items_retains_elapsed_time_and_is_account_bound():
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:1:');
now = 90000;
timer.activate('pull:repo:2:');
now = 120000;
const first = timer.snapshot('issue:repo:1:');
const second = timer.snapshot();
timer.stop();
now = 300000;
const stopped = timer.snapshot();
login = 'alexander';
const other = createTodayTimer({{storage,getLogin:() => login,now:() => now}}).snapshot();
process.stdout.write(JSON.stringify({{first,second,stopped,other}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"first": {"identity": "issue:repo:1:", "elapsed_ms": 90000, "running": False},
"second": {"identity": "pull:repo:2:", "elapsed_ms": 30000, "running": True},
"stopped": {"identity": "pull:repo:2:", "elapsed_ms": 30000, "running": False},
"other": {"identity": "", "elapsed_ms": 0, "running": False},
}
def test_today_timer_can_be_initialized_before_operator_identity_is_restored():
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 = '';
let now = 100;
const timer = createTodayTimer({{storage,getLogin:() => login,now:() => now}});
login = 'timmy';
const activated = timer.activate('issue:repo:1:');
now = 1100;
process.stdout.write(JSON.stringify({{activated,snapshot:timer.snapshot()}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"activated": True,
"snapshot": {"identity": "issue:repo:1:", "elapsed_ms": 1000, "running": True},
}
@pytest.mark.anyio
async def test_today_timer_is_wired_into_every_mobile_session_control():
html = await dashboard()
timer_source = TODAY_TIMER.read_text()
assert html.count('<button type="button" data-work-session-timer-toggle>') == 4
assert 'createTodayTimer({' in html
assert 'createTodayTimerView({' in html
assert 'timerView.open(workIdentity(item), workSession.checkpointed(item))' in html
assert 'active ? timer.activate(identity) : timer.stop()' in timer_source
assert "setInterval(timerView.render, 1000)" in html
assert 'timer.pause()' in timer_source
assert 'timer.resume()' in timer_source
assert "elapsed(snapshot.elapsed_ms)" in timer_source
assert "' / ' + formatEstimate(runway.current_minutes)" in timer_source
assert '.work-session-nav [data-work-session-timer-toggle] { min-height:44px;' in html
def test_active_today_session_reopens_current_item_without_restarting_checkpoint():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
+1
View File
@@ -678,6 +678,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/my-work.js",
"/dashboard/static/card-planning.js",
"/dashboard/static/today-work.js",
"/dashboard/static/today-timer.js",
"/dashboard/static/today-completion.js",
"/dashboard/static/today-readiness.js",
"/dashboard/static/comment-next.js",
+1 -1
View File
@@ -333,7 +333,7 @@ async def test_today_session_sheets_offer_a_touch_safe_plan_only_completion_acti
assert "document.querySelectorAll('[data-work-session-complete]')" in html
assert "completeTodayItem(selectedSessionItem(button.dataset.workSessionComplete))" in html
assert ".work-session-nav button { min-height:44px; width:100%; }" in html
assert "grid-template-columns:repeat(3,minmax(0,1fr))" in html
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in html
@pytest.mark.anyio