feat: continue timed breaks across devices (Closes #1038)
CI / lint (pull_request) Successful in 2m53s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Failing after 2m8s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 20:08:05 +00:00
1 parent 1d242bab75
commit c5b466f281
7 files changed
+172 -33

No files matched your search

+20 -1
View File
@@ -37,7 +37,10 @@ process.stdout.write(JSON.stringify({started,paused,restored}));
"deadline_at": 1012000,
"expired": False,
},
"paused": {"identity": "issue:r:42:", "elapsed_ms": 12000, "running": False},
"paused": {
"identity": "issue:r:42:", "elapsed_ms": 12000, "running": False,
"break_deadline_at": 1012000,
},
"restored": {
"identity": "issue:r:42:",
"deadline_at": 1012000,
@@ -46,6 +49,22 @@ process.stdout.write(JSON.stringify({started,paused,restored}));
}
def test_timed_break_snapshot_carries_deadline_for_cross_device_sync():
script = TIMER.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:()=>100000});
timer.activate('issue:r:42:');
timer.startBreak(15);
process.stdout.write(JSON.stringify(timer.snapshot()));
"""
assert run_node(script) == {
"identity": "issue:r:42:", "elapsed_ms": 0, "running": False,
"break_deadline_at": 1_000_000,
}
def test_break_expiry_never_restarts_time_and_resume_is_explicit_and_idempotent():
script = TIMER.read_text() + r"""
const values = new Map();
+50
View File
@@ -57,6 +57,56 @@ const sync = createTodaySessionSync({
}
def test_second_device_sees_and_resumes_a_timed_break():
result = run_node(
r"""
const calls=[];
const remote={revision:3,device_id:'phone-a',identity:'issue:r:42:',elapsed_ms:90000,running:false,break_deadline_at:1800000,updated_at:10};
let adopted=null;
const offers=[];
const sync=createTodaySessionSync({
getDeviceId:()=> 'desktop-b',
fetchJson:async (url,options={})=>{
calls.push(options.body ? JSON.parse(options.body) : null);
return options.method === 'PATCH' ? {...remote,revision:4,device_id:'desktop-b',running:true,break_deadline_at:null} : remote;
},
timer:{adopt:(identity,elapsed,running)=>{adopted={identity,elapsed,running};}},
onRemote:session=>offers.push(session),
});
(async()=>{
await sync.refresh();
const claimed=await sync.claim();
process.stdout.write(JSON.stringify({offers,claimed,adopted,calls}));
})().catch(error=>{console.error(error);process.exit(1);});
"""
)
assert result["offers"][0]["break_deadline_at"] == 1_800_000
assert result["calls"][1] == {
"base_revision": 3, "device_id": "desktop-b", "identity": "issue:r:42:",
"elapsed_ms": 90_000, "running": True, "break_deadline_at": None,
}
assert result["adopted"] == {
"identity": "issue:r:42:", "elapsed": 90_000, "running": True,
}
assert result["claimed"]["device_id"] == "desktop-b"
def test_break_handoff_summary_is_actionable_and_privacy_safe():
result = run_node(
r"""
const session={identity:'issue:private/repo:42:',elapsed_ms:90000,running:false,break_deadline_at:1800000};
process.stdout.write(JSON.stringify({
summary:todaySessionHandoffSummary(session,{title:'Secret issue'},0),
}));
"""
)
assert result["summary"].startswith("On break until ")
assert "Secret issue" not in result["summary"]
assert "private/repo" not in result["summary"]
def test_previous_owner_pauses_after_another_device_claims():
result = run_node(
r"""
+36
View File
@@ -22,6 +22,7 @@ def test_active_session_is_durable_and_account_scoped(tmp_path):
"identity": "issue:stackchain/dashboard:42:",
"elapsed_ms": 90_000,
"running": True,
"break_deadline_at": None,
"updated_at": 1234.5,
}
assert TodayStore(path).get_session("timmy") == saved
@@ -31,10 +32,26 @@ def test_active_session_is_durable_and_account_scoped(tmp_path):
"identity": "",
"elapsed_ms": 0,
"running": False,
"break_deadline_at": None,
"updated_at": None,
}
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)
saved = store.update_session(
"timmy", base_revision=0, device_id="phone-a",
identity="issue:r:42:", elapsed_ms=90_000, running=False,
break_deadline_at=1_800_000,
)
assert saved["break_deadline_at"] == 1_800_000
assert saved["running"] is False
assert TodayStore(path).get_session("timmy") == saved
def test_active_session_claim_rejects_a_stale_revision(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3")
first = store.update_session(
@@ -56,6 +73,25 @@ def test_active_session_claim_rejects_a_stale_revision(tmp_path):
assert store.get_session("timmy") == claimed
@pytest.mark.anyio
async def test_active_session_api_persists_a_bounded_break_deadline(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)
payload = main.TodaySessionUpdate(
base_revision=0, device_id="phone-a", identity="issue:r:1:",
elapsed_ms=5_000, running=False, break_deadline_at=1_800_000,
)
saved = await main.update_today_session(payload)
assert saved["break_deadline_at"] == 1_800_000
assert (await main.get_today_session()) == saved
@pytest.mark.anyio
async def test_active_session_api_returns_current_state_on_claim_conflict(monkeypatch, tmp_path):
async def user():