stackchain-dashboard/tests/test_today_work.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

198 lines
7.6 KiB
Python

import json
import subprocess
from pathlib import Path
import pytest
from tests.dashboard_bundle import dashboard
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
def run_node(script):
return subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout
def test_today_queue_is_account_scoped_ordered_unique_and_bounded():
script = f"""
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
const values = new Map();
const storage = {{
getItem: key => values.has(key) ? values.get(key) : null,
setItem: (key, value) => values.set(key, value),
removeItem: key => values.delete(key),
}};
let login = 'timmy';
const queue = createTodayWork({{storage, getLogin: () => login, limit: 3}});
const issue = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const first = queue.add(issue(1));
const duplicate = queue.add(issue(1));
queue.add(issue(2));
queue.add(issue(3));
const full = queue.add(issue(4));
queue.move(issue(3), 'up');
queue.move(issue(3), 'up');
const timmy = queue.reconcile([issue(1), issue(2), issue(3), issue(4)]).map(item => item.number);
const persisted = createTodayWork({{storage, getLogin: () => login, limit: 3}})
.reconcile([issue(1), issue(2), issue(3)]).map(item => item.number);
login = 'alexander';
const isolated = queue.reconcile([issue(1), issue(2), issue(3)]).map(item => item.number);
process.stdout.write(JSON.stringify({{first, duplicate, full, timmy, persisted, isolated}}));
"""
assert json.loads(run_node(script)) == {
"first": "added",
"duplicate": "exists",
"full": "full",
"timmy": [3, 1, 2],
"persisted": [3, 1, 2],
"isolated": [],
}
def test_today_queue_exposes_reorder_boundaries_for_touch_controls():
script = f"""
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
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 queue = createTodayWork({{storage, getLogin: () => 'timmy'}});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number}});
queue.add(item(1)); queue.add(item(2)); queue.add(item(3));
process.stdout.write(JSON.stringify([
queue.position(item(1)), queue.position(item(2)), queue.position(item(3)), queue.position(item(9))
]));
"""
assert json.loads(run_node(script)) == [
{"can_up": False, "can_down": True},
{"can_up": True, "can_down": True},
{"can_up": True, "can_down": False},
{"can_up": False, "can_down": False},
]
def test_authoritative_reconciliation_reports_retired_ids_before_local_prune():
script = f"""
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
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 queue = createTodayWork({{storage, getLogin: () => 'timmy'}});
const item = number => ({{kind:'issue', repository:'r', number}});
queue.add(item(1));
queue.add(item(2));
const reports = [];
const visible = queue.reconcile([item(2)], {{
pruneMissing: true,
onPrune: retiredIds => reports.push({{retiredIds, idsDuringReport: queue.read()}}),
}});
queue.reconcile([item(2)], {{
pruneMissing: true,
onPrune: retiredIds => reports.push({{retiredIds, idsDuringReport: queue.read()}}),
}});
process.stdout.write(JSON.stringify({{reports, visible:visible.map(entry => entry.number), stored:queue.read()}}));
"""
assert json.loads(run_node(script)) == {
"reports": [{
"retiredIds": ["issue:r:1:"],
"idsDuringReport": ["issue:r:1:", "issue:r:2:"],
}],
"visible": [2],
"stored": ["issue:r:2:"],
}
def test_failed_retirement_queue_keeps_local_identity_for_retry():
script = f"""
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
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 queue = createTodayWork({{storage, getLogin: () => 'timmy'}});
const item = number => ({{kind:'issue', repository:'r', number}});
queue.add(item(1));
const visible = queue.reconcile([], {{pruneMissing:true, onPrune:() => false}});
process.stdout.write(JSON.stringify({{visible, stored:queue.read()}}));
"""
assert json.loads(run_node(script)) == {
"visible": [],
"stored": ["issue:r:1:"],
}
def test_today_queue_adopts_bounded_server_order():
script = f"""
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
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 queue = createTodayWork({{storage, getLogin: () => 'timmy', limit: 2}});
queue.add({{kind:'issue', repository:'r', number:1}});
const adopted = queue.replace(['issue:r:3:', 'issue:r:2:', 'issue:r:2:', '', 'issue:r:1:']);
process.stdout.write(JSON.stringify({{adopted, ids:queue.read()}}));
"""
assert json.loads(run_node(script)) == {
"adopted": True,
"ids": ["issue:r:3:", "issue:r:2:"],
}
@pytest.mark.anyio
async def test_dashboard_runs_the_curated_today_queue_as_a_mobile_work_flow():
html = await dashboard()
assert '<script src="static/today-work.js"></script>' in html
assert '<script src="static/today-sync.js"></script>' in html
assert 'data-work-filter="today"' in html
assert 'data-work-count="today"' in html
assert "const todayWork = createTodayWork({" in html
assert "const filter = todayMyWork.length ? 'today' : (counts.attention ? 'attention' : 'all');" in html
assert "selectedWorkFilter === 'today' ? todayMyWork : activeMyWork" in html
assert 'data-today-add' in html
assert 'data-today-remove' in html
assert 'data-today-move="up"' in html
assert 'data-today-move="down"' in html
assert 'Today is limited to 5 items' in html
assert '.today-actions button' in html and 'min-height:44px' in html
assert 'id="today-sync-status"' in html
assert "todaySync.enqueue('add'" in html
assert "todaySync.enqueue('remove'" in html
assert "todaySync.enqueue('move'" in html
assert "const authoritativeTodayReconciliation = liveMode && hasContextSnapshot &&" in html
assert "!lastContextSnapshot?.error &&" in html
assert "onPrune: retiredIds =>" in html
assert "retiredIds.map(id => todaySync.enqueue('remove', id)).every(Boolean)" in html
assert "if (queued) todaySync.flush();" in html
assert "todaySync.flush();" in html
assert "Another device filled Today · showing its saved plan." in html
@pytest.mark.anyio
async def test_dashboard_activates_today_lifecycle_convergence():
html = await dashboard()
assert "todaySync.startLifecycle({ window, document });" in html
@pytest.mark.anyio
async def test_retained_authenticated_context_keeps_local_planning_separate_from_fresh_delivery():
html = await dashboard()
assert "let planningOwnerLogin = '';" in html
assert html.count("getLogin: () => planningOwnerLogin") == 3
assert "const retainedPlanningLogin = !snapshot.context.error ?" in html
assert "planningOwnerLogin = retainedPlanningLogin;" in html
assert "button.disabled = !planningOwnerLogin;" in html
assert "activeFlushLogin = contextIdentityFresh ?" in html
assert "getOwnerLogin: () => confirmedOwnerLogin" in html
assert "Planning is unavailable until your operator identity is restored." in html
assert "Could not save Today on this device." in html
assert "Could not save Later on this device." in html
assert "data-planning-disabled" in html