feat: preserve Today timing ledger across handoff (Closes #1276)
CI / lint (pull_request) Successful in 3m48s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m42s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-22 19:38:30 +00:00
1 parent e0d8ab9f10
commit 548b95ddb6
9 files changed
+267 -20

No files matched your search

+1 -1
View File
@@ -23,5 +23,5 @@ def test_dashboard_packages_a_mobile_today_session_handoff():
assert "Session offline · will retry." in today_bundle
assert "Session continued on another device." in today_bundle
assert "todaySessionSync = attachTodaySessionHandoff" in dashboard
assert "todaySessionSync?.publish(snapshot)" in dashboard
assert "todaySessionSync?.publish(timer.sessionSnapshot())" in dashboard
assert "min-height:44px" in css
+107
View File
@@ -58,6 +58,43 @@ const sync = createTodaySessionSync({
}
def test_claim_adopts_the_complete_timing_ledger_for_recap():
result = run_node(
f"""
const createTimer = require({json.dumps(str(TIMER))});
const values = new Map();
const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}};
const timer = createTimer({{storage,getLogin:()=> 'timmy',now:()=>100000}});
const remote = {{
revision:3,device_id:'desktop-a',identity:'issue:r:2:',elapsed_ms:90000,running:true,
entries:[
{{identity:'issue:r:1:',elapsed_ms:60000}},
{{identity:'issue:r:2:',elapsed_ms:90000}},
],
}};
const sync=createTodaySessionSync({{
getDeviceId:()=> 'phone-b',timer,
fetchJson:async (_url,options={{}})=>options.method ?
{{...JSON.parse(options.body),revision:4,device_id:'phone-b'}} : remote,
}});
(async()=>{{
await sync.refresh();
await sync.claim();
const snapshot=timer.sessionSnapshot();
const recap=timer.recapEntries();
process.stdout.write(JSON.stringify({{recap,snapshot}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
)
assert result["recap"] == [
{"identity": "issue:r:1:", "elapsed_ms": 60_000},
{"identity": "issue:r:2:", "elapsed_ms": 90_000},
]
assert result["snapshot"]["identity"] == "issue:r:2:"
assert result["snapshot"]["running"] is True
def test_second_device_sees_and_resumes_a_timed_break():
result = run_node(
r"""
@@ -145,6 +182,25 @@ process.stdout.write(JSON.stringify({
assert "private/repo" not in result["summary"]
def test_multi_item_handoff_summary_shows_scope_and_total_without_titles():
result = run_node(
r"""
const session={
identity:'issue:private/repo:42:',elapsed_ms:90000,running:true,
entries:[
{identity:'issue:private/repo:41:',elapsed_ms:60000},
{identity:'issue:private/repo:42:',elapsed_ms:90000},
],
};
process.stdout.write(JSON.stringify(todaySessionHandoffSummary(session,{title:'Secret issue'})));
"""
)
assert result == "2 tracked items · 2 min total"
assert "Secret" not in result
assert "private/repo" not in result
def test_previous_owner_pauses_after_another_device_claims():
result = run_node(
r"""
@@ -403,3 +459,54 @@ const sync = createTodaySessionSync({
assert result["calls"] == ["GET", "PATCH"]
assert result["statuses"][-1] == "offline"
assert "conflict" not in result["statuses"]
def test_owner_publish_includes_every_timed_item():
result = run_node(
f"""
const createTimer = require({json.dumps(str(TIMER))});
const values = new Map();
const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}};
let now=0;
const timer=createTimer({{storage,getLogin:()=> 'timmy',now:()=>now}});
timer.activate('issue:r:1:'); now=60000;
timer.activate('issue:r:2:'); now=150000;
const bodies=[];
const owned={{revision:1,device_id:'desktop-a',identity:'issue:r:2:',elapsed_ms:90000,running:true}};
const sync=createTodaySessionSync({{
getDeviceId:()=> 'desktop-a',timer,
fetchJson:async (_url,options={{}})=>{{
if (!options.method) return owned;
const body=JSON.parse(options.body); bodies.push(body);
return {{...body,revision:2,updated_at:1}};
}},
}});
(async()=>{{
await sync.refresh();
await sync.publish();
process.stdout.write(JSON.stringify(bodies[0].entries));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
)
assert result == [
{"identity": "issue:r:1:", "elapsed_ms": 60_000},
{"identity": "issue:r:2:", "elapsed_ms": 90_000},
]
def test_timer_snapshot_samples_active_elapsed_once_for_a_consistent_ledger():
result = run_node(
f"""
const createTimer = require({json.dumps(str(TIMER))});
const values=new Map();
const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}};
let ticks=0;
const timer=createTimer({{storage,getLogin:()=> 'timmy',now:()=>++ticks*1000}});
timer.activate('issue:r:1:');
const snapshot=timer.sessionSnapshot();
process.stdout.write(JSON.stringify(snapshot));
"""
)
assert result["elapsed_ms"] == result["entries"][0]["elapsed_ms"]
+60
View File
@@ -127,6 +127,9 @@ def test_active_session_is_durable_and_account_scoped(tmp_path):
"device_id": "phone-a",
"identity": "issue:stackchain/dashboard:42:",
"elapsed_ms": 90_000,
"entries": [{
"identity": "issue:stackchain/dashboard:42:", "elapsed_ms": 90_000,
}],
"running": True,
"break_deadline_at": None,
"updated_at": 1234.5,
@@ -137,12 +140,30 @@ def test_active_session_is_durable_and_account_scoped(tmp_path):
"device_id": "",
"identity": "",
"elapsed_ms": 0,
"entries": [],
"running": False,
"break_deadline_at": None,
"updated_at": None,
}
def test_active_session_preserves_complete_bounded_timing_ledger(tmp_path):
path = tmp_path / "today.sqlite3"
store = TodayStore(path, clock=lambda: 1234.5)
entries = [
{"identity": "issue:r:1:", "elapsed_ms": 60_000},
{"identity": "issue:r:2:", "elapsed_ms": 90_000},
]
saved = store.update_session(
"timmy", base_revision=0, device_id="phone-a",
identity="issue:r:2:", elapsed_ms=90_000, running=True, entries=entries,
)
assert saved["entries"] == entries
assert TodayStore(path).get_session("timmy")["entries"] == entries
def test_timed_break_deadline_is_durable_in_the_account_session(tmp_path):
path = tmp_path / "today.sqlite3"
store = TodayStore(path, clock=lambda: 1234.5)
@@ -198,6 +219,45 @@ async def test_active_session_api_persists_a_bounded_break_deadline(monkeypatch,
assert (await main.get_today_session()) == saved
@pytest.mark.anyio
async def test_active_session_api_persists_complete_timing_ledger(monkeypatch, tmp_path):
async def user():
return {"login": "timmy"}
store = TodayStore(tmp_path / "today.sqlite3")
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main, "_today_store", lambda: store)
entries = [
{"identity": "issue:r:1:", "elapsed_ms": 5_000},
{"identity": "issue:r:2:", "elapsed_ms": 8_000},
]
payload = main.TodaySessionUpdate(
base_revision=0, device_id="phone-a", identity="issue:r:2:",
elapsed_ms=8_000, running=True, entries=entries,
)
saved = await main.update_today_session(payload)
assert saved["entries"] == entries
assert (await main.get_today_session())["entries"] == entries
def test_active_session_api_rejects_duplicate_or_inconsistent_ledger():
base = {
"base_revision": 0, "device_id": "phone-a", "identity": "issue:r:1:",
"elapsed_ms": 5_000, "running": True,
}
with pytest.raises(ValueError, match="unique"):
main.TodaySessionUpdate(**base, entries=[
{"identity": "issue:r:1:", "elapsed_ms": 5_000},
{"identity": "issue:r:1:", "elapsed_ms": 5_000},
])
with pytest.raises(ValueError, match="match"):
main.TodaySessionUpdate(**base, entries=[
{"identity": "issue:r:1:", "elapsed_ms": 4_999},
])
def test_active_session_api_rejects_a_running_break():
with pytest.raises(ValueError, match="break must remain paused"):
main.TodaySessionUpdate(