174 lines
6.3 KiB
Python
174 lines
6.3 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",
|
|
}
|