523 lines
22 KiB
Python
523 lines
22 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
TODAY_SYNC = Path(__file__).parents[1] / "frontend" / "today-sync.js"
|
|
COORDINATOR = Path(__file__).parents[1] / "frontend" / "outbox-coordinator.js"
|
|
|
|
|
|
def test_two_tabs_cannot_erase_each_others_today_edits_or_double_drain():
|
|
script = f"""
|
|
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
|
const createCoordinator = require({json.dumps(str(COORDINATOR))});
|
|
const values = new Map();
|
|
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
|
|
getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
|
|
const channels=[]; const channelFactory=()=>{{const channel={{onmessage:null,postMessage:data=>channels.filter(x=>x!==channel).forEach(x=>x.onmessage?.({{data}})),close(){{}}}};channels.push(channel);return channel}};
|
|
const held=new Set(); const locks={{request:async(name,_options,work)=>{{if(held.has(name))return work(null);held.add(name);try{{return await work({{name}})}}finally{{held.delete(name)}}}}}};
|
|
let sequence=0,releaseFirst;const delivered=[];
|
|
const make=tab=>createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}),
|
|
fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,ids:[]}};const operation=JSON.parse(options.body).operations[0];delivered.push(operation.operation_id);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,ids:delivered}}}},onRemoteIds:()=>{{}},onStatus:()=>{{}}}});
|
|
const first=make('first'),second=make('second');first.enqueue('add','issue:r:1:');
|
|
(async()=>{{const draining=first.flush();while(!releaseFirst)await Promise.resolve();second.enqueue('add','issue:r:2:');const competing=second.flush();releaseFirst();await Promise.all([draining,competing]);await first.flush();process.stdout.write(JSON.stringify({{delivered,pending:first.pending(),keys:[...values.keys()]}}));}})();
|
|
"""
|
|
result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)
|
|
assert sorted(result["delivered"]) == ["first-1", "second-2"]
|
|
assert len(result["delivered"]) == 2
|
|
assert result["pending"] == []
|
|
assert not any(".operation." in key for key in result["keys"])
|
|
|
|
|
|
def test_operations_enqueued_during_an_inflight_flush_are_drained_before_it_settles():
|
|
script = f"""
|
|
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
|
const values = new Map();
|
|
const patches = [];
|
|
let sequence = 0;
|
|
let releaseFirstPatch;
|
|
const firstPatchBlocked = new Promise(resolve => {{ globalThis.firstPatchStarted = resolve; }});
|
|
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: () => 'op-' + (++sequence),
|
|
fetchJson: async (_url, options={{}}) => {{
|
|
if (!options.method) return {{revision:0, ids:[]}};
|
|
const operation = JSON.parse(options.body).operations[0];
|
|
patches.push(operation);
|
|
if (patches.length === 1) {{
|
|
globalThis.firstPatchStarted();
|
|
await new Promise(resolve => {{ releaseFirstPatch = resolve; }});
|
|
}}
|
|
return {{revision:patches.length, ids:patches.map(item => item.item_id)}};
|
|
}},
|
|
onRemoteIds: ids => {{ globalThis.ids = ids; }},
|
|
onStatus: status => {{ globalThis.status = status; }},
|
|
}});
|
|
sync.enqueue('add', 'issue:r:1:');
|
|
(async () => {{
|
|
const firstFlush = sync.flush();
|
|
await firstPatchBlocked;
|
|
sync.enqueue('add', 'issue:r:2:');
|
|
const sharedFlush = sync.flush();
|
|
const sharesInflight = firstFlush === sharedFlush;
|
|
releaseFirstPatch();
|
|
await sharedFlush;
|
|
process.stdout.write(JSON.stringify({{
|
|
sharesInflight, patches, 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 == {
|
|
"sharesInflight": True,
|
|
"patches": [
|
|
{"operation_id": "op-1", "action": "add", "item_id": "issue:r:1:", "direction": None, "base_revision": 0},
|
|
{"operation_id": "op-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0},
|
|
],
|
|
"pending": [],
|
|
"ids": ["issue:r:1:", "issue:r:2:"],
|
|
"status": "saved",
|
|
}
|
|
|
|
|
|
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v61" in source
|
|
assert "BASE + 'static/today-sync.js'" in source
|
|
|
|
|
|
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:', JSON.parse(options.body).operations[0].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",
|
|
]
|
|
assert result["requests"][0]["body"] == {"operations": [{
|
|
"operation_id": "fixed-op",
|
|
"action": "add",
|
|
"item_id": "issue:r:2:",
|
|
"direction": None,
|
|
"base_revision": 0,
|
|
}]}
|
|
assert result["adopted"] == ["issue:r:9:", "issue:r:2:"]
|
|
assert result["status"] == "saved"
|
|
assert result["pending"] == []
|
|
|
|
|
|
def test_today_operations_capture_server_revision_and_expire_before_replay():
|
|
script = f"""
|
|
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
|
const values = new Map(); const statuses=[]; const requests=[];
|
|
const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
|
|
getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
|
|
const sync=createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'old-op',
|
|
now:()=>1_800_000_000_000,maxOfflineMs:1000,
|
|
fetchJson:async(_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:7,ids:['server']}}}},
|
|
onRemoteIds:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.count||0])}});
|
|
(async()=>{{await sync.flush();sync.enqueue('move','issue:r:2:','up');
|
|
const pendingBefore=sync.pending();
|
|
const key=[...values.keys()].find(value=>value.includes('.operation.'));
|
|
const record=JSON.parse(values.get(key));record.queued_at=1_799_999_000_000;values.set(key,JSON.stringify(record));
|
|
await sync.flush();process.stdout.write(JSON.stringify({{pendingBefore,requests,statuses,pendingAfter:sync.pending()}}));}})();
|
|
"""
|
|
result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)
|
|
|
|
assert result["pendingBefore"] == [{
|
|
"operation_id": "old-op", "action": "move", "item_id": "issue:r:2:",
|
|
"direction": "up", "base_revision": 7,
|
|
}]
|
|
assert result["requests"] == ["GET", "GET"]
|
|
assert ["expired", 1] in result["statuses"]
|
|
assert result["statuses"][-1] == ["expired", 1]
|
|
assert result["pendingAfter"] == []
|
|
|
|
|
|
def test_pending_today_edits_are_sent_as_one_batch_without_a_preflight_get():
|
|
script = f"""
|
|
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
|
const values = new Map(); const requests=[]; 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:()=> 'op-'+(++sequence),
|
|
fetchJson:async (_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:2,ids:['issue:r:1:','issue:r:2:'],accepted_operation_ids:body.operations.map(x=>x.operation_id),duplicate_operation_ids:[],rejected_operations:[]}}}},
|
|
onRemoteIds:()=>{{}},onStatus:()=>{{}},
|
|
}});
|
|
sync.enqueue('add','issue:r:1:');sync.enqueue('add','issue:r:2:');
|
|
(async()=>{{await sync.flush();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 len(result["requests"]) == 1
|
|
assert [item["operation_id"] for item in result["requests"][0]["operations"]] == ["op-1", "op-2"]
|
|
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",
|
|
"base_revision": 0,
|
|
}],
|
|
"status": "retrying",
|
|
}
|
|
|
|
|
|
def test_retryable_failure_replays_automatically_once_with_same_operation_id():
|
|
script = f"""
|
|
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
|
const values = new Map(); const timers = []; const statuses = []; const patches = [];
|
|
let attempts = 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:()=> 'stable-op',
|
|
setTimer:(callback,delay)=>{{timers.push({{callback,delay}});return timers.length}}, clearTimer:()=>{{}},
|
|
fetchJson:async (_url,options={{}})=>{{
|
|
attempts += 1;
|
|
if (attempts === 1) {{ const error = new Error('busy'); error.status=503; error.retryAfter=2; throw error; }}
|
|
if (options.method) patches.push(...JSON.parse(options.body).operations);
|
|
return options.method ? {{revision:1,ids:['issue:r:2:']}} : {{revision:0,ids:[]}};
|
|
}},
|
|
onRemoteIds:()=>{{}}, onStatus:(state,detail)=>statuses.push([state,detail?.delayMs||null]),
|
|
}});
|
|
sync.enqueue('add','issue:r:2:');
|
|
(async()=>{{
|
|
await sync.flush();
|
|
const scheduled = timers.map(timer=>timer.delay);
|
|
await timers[0].callback();
|
|
process.stdout.write(JSON.stringify({{scheduled,attempts,patches,pending:sync.pending(),statuses}}));
|
|
}})();
|
|
"""
|
|
result = json.loads(
|
|
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
|
)
|
|
|
|
assert result["scheduled"] == [2000]
|
|
assert result["attempts"] == 2
|
|
assert result["patches"] == [{
|
|
"operation_id": "stable-op", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0,
|
|
}]
|
|
assert result["pending"] == []
|
|
assert ["retrying", 2000] in result["statuses"]
|
|
assert result["statuses"][-1] == ["saved", None]
|
|
|
|
|
|
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,
|
|
"base_revision": 0,
|
|
}],
|
|
"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, "base_revision": 0},
|
|
{"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0},
|
|
],
|
|
}
|
|
|
|
|
|
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={{}}) => {{
|
|
patch = true;
|
|
return {{revision:5, ids:['1','2','3','4','5'], accepted_operation_ids:[], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'sixth',reason:'today_full'}}]}};
|
|
}},
|
|
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_server_limit_conflict_drops_only_the_rejected_operation_and_drains_later_edits():
|
|
script = f"""
|
|
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
|
const values = new Map();
|
|
const patches = [];
|
|
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: () => 'op-' + (++sequence),
|
|
fetchJson: async (_url, options={{}}) => {{
|
|
const operations = JSON.parse(options.body).operations;
|
|
patches.push(...operations);
|
|
return {{revision:6, ids:['1','2','3','4'], accepted_operation_ids:['op-2'], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'op-1',reason:'today_full'}}]}};
|
|
}},
|
|
onRemoteIds: ids => {{ globalThis.ids = ids; }},
|
|
onStatus: status => statuses.push(status),
|
|
}});
|
|
sync.enqueue('add', '6');
|
|
sync.enqueue('remove', '5');
|
|
(async()=>{{
|
|
const result = await sync.flush();
|
|
process.stdout.write(JSON.stringify({{result, patches, pending:sync.pending(), ids:globalThis.ids, statuses}}));
|
|
}})();
|
|
"""
|
|
result = json.loads(
|
|
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
|
)
|
|
|
|
assert result == {
|
|
"result": False,
|
|
"patches": [
|
|
{"operation_id": "op-1", "action": "add", "item_id": "6", "direction": None, "base_revision": 0},
|
|
{"operation_id": "op-2", "action": "remove", "item_id": "5", "direction": None, "base_revision": 0},
|
|
],
|
|
"pending": [],
|
|
"ids": ["1", "2", "3", "4"],
|
|
"statuses": ["pending", "pending", "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": ["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"]]
|