stackchain-dashboard/tests/test_today_sync.py
timmy 3c743a2b71
All checks were successful
CI / lint (pull_request) Successful in 42s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped
feat: converge Today sync across active devices (#361)
2026-08-09 02:01:55 +00:00

310 lines
11 KiB
Python

import json
import subprocess
from pathlib import Path
TODAY_SYNC = Path(__file__).parents[1] / "frontend" / "today-sync.js"
def test_local_operations_replay_once_then_adopt_server_order():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const storage = {{
getItem: key => values.get(key) || null,
setItem: (key, value) => values.set(key, value),
removeItem: key => values.delete(key),
}};
const requests = [];
let remote = {{revision: 1, ids:['issue:r:9:']}};
const sync = createTodaySync({{
storage,
getLogin: () => 'timmy',
createOperationId: () => 'fixed-op',
fetchJson: async (url, options={{}}) => {{
requests.push({{url, body: options.body && JSON.parse(options.body)}});
if (!options.method) return remote;
remote = {{revision: remote.revision + 1, ids:['issue:r:9:', options.body && JSON.parse(options.body).item_id]}};
return remote;
}},
onRemoteIds: ids => {{ globalThis.adopted = ids; }},
onStatus: status => {{ globalThis.status = status; }},
}});
sync.enqueue('add', 'issue:r:2:');
(async () => {{
await sync.flush();
await sync.flush();
process.stdout.write(JSON.stringify({{
requests, adopted: globalThis.adopted, status: globalThis.status,
pending: sync.pending(),
}}));
}})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert [request["url"] for request in result["requests"]] == [
"api/v1/today",
"api/v1/today",
"api/v1/today",
]
assert result["requests"][1]["body"] == {
"operation_id": "fixed-op",
"action": "add",
"item_id": "issue:r:2:",
"direction": None,
}
assert result["adopted"] == ["issue:r:9:", "issue:r:2:"]
assert result["status"] == "saved"
assert result["pending"] == []
def test_failed_delivery_stays_pending_for_offline_replay():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin: () => 'timmy', createOperationId: () => 'offline-op',
fetchJson: async () => {{ throw new Error('offline'); }},
onRemoteIds: () => {{}}, onStatus: value => {{ globalThis.status = value; }},
}});
sync.enqueue('move', 'issue:r:2:', 'up');
(async () => {{ await sync.flush(); process.stdout.write(JSON.stringify({{pending:sync.pending(),status:globalThis.status}})); }})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == {
"pending": [{
"operation_id": "offline-op",
"action": "move",
"item_id": "issue:r:2:",
"direction": "up",
}],
"status": "pending",
}
def test_duplicate_pending_retirements_collapse_to_one_effective_remove():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
let sequence = 0;
const statuses = [];
const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin: () => 'timmy', createOperationId: () => 'remove-' + (++sequence),
fetchJson: async () => ({{revision:0,ids:[]}}), onRemoteIds:()=>{{}}, onStatus:value=>statuses.push(value),
}});
const first = sync.enqueue('remove', 'issue:r:1:');
const duplicate = sync.enqueue('remove', 'issue:r:1:');
process.stdout.write(JSON.stringify({{first, duplicate, pending:sync.pending(), statuses}}));
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == {
"first": True,
"duplicate": True,
"pending": [{
"operation_id": "remove-1",
"action": "remove",
"item_id": "issue:r:1:",
"direction": None,
}],
"statuses": ["pending", "pending"],
}
def test_existing_device_queue_is_migrated_only_once():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
let sequence = 0;
const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin: () => 'timmy', createOperationId: () => 'migration-' + (++sequence),
fetchJson: async () => ({{revision:0,ids:[]}}), onRemoteIds:()=>{{}}, onStatus:()=>{{}},
}});
const first = sync.migrate(['issue:r:1:', 'issue:r:2:']);
const second = sync.migrate(['issue:r:1:', 'issue:r:2:']);
process.stdout.write(JSON.stringify({{first, second, pending:sync.pending()}}));
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == {
"first": True,
"second": False,
"pending": [
{"operation_id": "migration-1", "action": "add", "item_id": "issue:r:1:", "direction": None},
{"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None},
],
}
def test_server_limit_conflict_drops_rejected_add_and_adopts_server_truth():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
let patch = false;
const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin: () => 'timmy', createOperationId: () => 'sixth',
fetchJson: async (_url, options={{}}) => {{
if (options.method) {{ patch = true; const error = new Error('full'); error.status = 409; throw error; }}
return {{revision:5, ids:['1','2','3','4','5']}};
}},
onRemoteIds:ids=>{{globalThis.ids=ids}}, onStatus:value=>{{globalThis.status=value}},
}});
sync.enqueue('add', '6');
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{patch,pending:sync.pending(),ids:globalThis.ids,status:globalThis.status}}));}})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == {
"patch": True,
"pending": [],
"ids": ["1", "2", "3", "4", "5"],
"status": "full",
}
def test_tabs_ignore_an_older_response_after_a_newer_revision_is_broadcast():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const listeners = [];
const createChannel = () => ({{
addEventListener: (_name, listener) => listeners.push(listener),
postMessage: data => listeners.forEach(listener => listener({{data}})),
}});
let resolveOld;
let resolveNew;
const oldHistory = [];
const newHistory = [];
const oldTab = createTodaySync({{
storage, getLogin:()=> 'timmy', createChannel,
fetchJson:()=>new Promise(resolve=>{{resolveOld=resolve}}),
onRemoteIds:ids=>oldHistory.push(ids), onStatus:()=>{{}},
}});
const newTab = createTodaySync({{
storage, getLogin:()=> 'timmy', createChannel,
fetchJson:()=>new Promise(resolve=>{{resolveNew=resolve}}),
onRemoteIds:ids=>newHistory.push(ids), onStatus:()=>{{}},
}});
(async()=>{{
const oldFlush = oldTab.flush();
const newFlush = newTab.flush();
await Promise.resolve();
resolveNew({{revision:2,ids:['new']}});
await newFlush;
resolveOld({{revision:1,ids:['old']}});
await oldFlush;
process.stdout.write(JSON.stringify({{oldHistory,newHistory}}));
}})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result["oldHistory"][-1] == ["new"]
assert result["newHistory"][-1] == ["new"]
def test_returning_online_replays_pending_operations():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const handlers = {{}};
const requests = [];
let revision = 1;
const sync = createTodaySync({{
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin:()=> 'timmy', createOperationId:()=> 'offline-add',
fetchJson:async (_url, options={{}})=>{{
requests.push(options.method || 'GET');
if (options.method) return {{revision:++revision,ids:['issue:r:1:']}};
return {{revision,ids:[]}};
}},
onRemoteIds:()=>{{}}, onStatus:()=>{{}},
}});
sync.enqueue('add', 'issue:r:1:');
sync.startLifecycle({{
window: {{addEventListener:(name, handler)=>{{handlers[name]=handler}}}},
document: {{addEventListener:()=>{{}}, hidden:false}},
}});
(async()=>{{
await handlers.online();
process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));
}})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == {"requests": ["GET", "PATCH"], "pending": []}
def test_foregrounding_a_stale_tab_refreshes_the_saved_plan():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const handlers = {{}};
const adopted = [];
const documentObject = {{
hidden:true,
addEventListener:(name, handler)=>{{handlers[name]=handler}},
}};
const sync = createTodaySync({{
storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
getLogin:()=> 'timmy',
fetchJson:async ()=>({{revision:7,ids:['issue:r:7:']}}),
onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}},
}});
sync.startLifecycle({{
window: {{addEventListener:()=>{{}}}}, document:documentObject,
}});
(async()=>{{
await handlers.visibilitychange();
documentObject.hidden = false;
await handlers.visibilitychange();
process.stdout.write(JSON.stringify(adopted));
}})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == [["issue:r:7:"]]
def test_old_account_broadcasts_are_ignored_after_identity_changes():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
let login = 'timmy';
let listener;
const adopted = [];
const sync = createTodaySync({{
storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
getLogin:()=>login,
createChannel:()=>({{addEventListener:(_name, handler)=>{{listener=handler}},postMessage:()=>{{}},close:()=>{{}}}}),
fetchJson:async ()=>({{revision:1,ids:['timmy-plan']}}),
onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}},
}});
(async()=>{{
await sync.flush();
login = 'alexander';
listener({{data:{{revision:2,ids:['timmy-secret']}}}});
process.stdout.write(JSON.stringify(adopted));
}})();
"""
result = json.loads(
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
)
assert result == [["timmy-plan"]]