stackchain-dashboard/tests/test_today_session_sync.py
timmy 8d12a0384b
All checks were successful
CI / lint (pull_request) Successful in 3m11s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m22s
CI / release-candidate (pull_request) Has been skipped
feat: hand off active Today sessions across devices (Closes #1022)
2026-08-17 12:44:01 +00:00

122 lines
3.7 KiB
Python

import json
import subprocess
from pathlib import Path
SOURCE = Path(__file__).parents[1] / "frontend" / "today-session-sync.js"
def run_node(script: str) -> dict:
completed = subprocess.run(
["node", "-e", SOURCE.read_text() + "\n" + script],
check=True,
capture_output=True,
text=True,
)
return json.loads(completed.stdout)
def test_second_device_sees_and_claims_the_running_session():
result = run_node(
r"""
const calls = [];
const remote = {revision:3, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:90000, running:true, updated_at:10};
let adopted = null;
const offers = [];
const sync = createTodaySessionSync({
getDeviceId:()=>'phone-b',
fetchJson:async (url, options={}) => {
calls.push({url, body:options.body ? JSON.parse(options.body) : null});
return options.method === 'PATCH' ? {...remote, revision:4, device_id:'phone-b'} : 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]["device_id"] == "desktop-a"
assert result["claimed"]["device_id"] == "phone-b"
assert result["adopted"] == {
"identity": "issue:r:42:", "elapsed": 90000, "running": True,
}
assert result["calls"][1] == {
"url": "api/v1/today/session",
"body": {
"base_revision": 3,
"device_id": "phone-b",
"identity": "issue:r:42:",
"elapsed_ms": 90000,
"running": True,
},
}
def test_previous_owner_pauses_after_another_device_claims():
result = run_node(
r"""
let response = {revision:1, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true};
let pauses = 0;
let transferred = null;
const sync = createTodaySessionSync({
getDeviceId:()=> 'desktop-a',
fetchJson:async()=>response,
timer:{pause:()=>{pauses += 1;}},
onTransferred:session=>{transferred=session;},
});
(async()=>{
await sync.refresh();
response = {...response, revision:2, device_id:'phone-b', elapsed_ms:2500};
await sync.refresh();
await sync.refresh();
process.stdout.write(JSON.stringify({pauses, transferred}));
})().catch(error=>{console.error(error);process.exit(1);});
"""
)
assert result["pauses"] == 1
assert result["transferred"]["device_id"] == "phone-b"
def test_owner_poll_publishes_current_elapsed_time():
result = run_node(
r"""
const calls = [];
let tick = null;
let elapsed = 1000;
const owned = {revision:1, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true};
const sync = createTodaySessionSync({
getDeviceId:()=> 'desktop-a',
fetchJson:async (url, options={}) => {
calls.push({method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null});
return options.method === 'PATCH' ? {...owned, revision:2, elapsed_ms:elapsed} : owned;
},
timer:{snapshot:()=>({identity:owned.identity, elapsed_ms:elapsed, running:true})},
setInterval:callback=>{tick=callback; return 7;},
});
(async()=>{
await sync.refresh();
sync.start();
elapsed = 6500;
await tick();
process.stdout.write(JSON.stringify(calls));
})().catch(error=>{console.error(error);process.exit(1);});
"""
)
assert result[-1] == {
"method": "PATCH",
"body": {
"base_revision": 1,
"device_id": "desktop-a",
"identity": "issue:r:42:",
"elapsed_ms": 6500,
"running": True,
},
}