feat: defer selected updates as unread batch (Closes #659)
CI / lint (pull_request) Successful in 1m36s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-12 15:11:51 +00:00
1 parent 7421a8f62d
commit ea3712ca3e
6 files changed
+109 -3

No files matched your search

+67
View File
@@ -1343,6 +1343,59 @@ process.stdout.write(JSON.stringify({{noIdentity, invalid, failedWrite}}));
}
def test_later_queue_defers_a_batch_atomically_with_one_wake_time_and_change_per_item():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
const values = new Map();
const changes = [];
let writes = 0;
let fail = false;
const storage = {{
getItem:key => values.get(key) || null,
setItem:(key,value) => {{ writes += 1; if (fail) throw new Error('quota'); values.set(key,value); }},
removeItem:key => values.delete(key),
}};
const first = {{kind:'update',repository:'stackchain/api',number:17,notification_id:91}};
const second = {{kind:'update',repository:'stackchain/web',number:8,notification_id:92}};
const store = createLaterWork({{
storage, getLogin:() => 'timmy', now:() => new Date('2026-08-08T12:00:00Z'),
setTimer:() => 1, clearTimer:() => {{}},
onChange:(...change) => changes.push(change),
}});
const deferred = store.deferMany([first, second], new Date('2026-08-09T09:00:00Z'));
const saved = store.partition([first, second]);
const beforeFailure = JSON.stringify(store.read());
fail = true;
const unavailable = store.deferMany([
{{kind:'update',repository:'stackchain/api',number:19,notification_id:93}},
{{kind:'update',repository:'stackchain/api',number:20,notification_id:94}},
], new Date('2026-08-10T09:00:00Z'));
process.stdout.write(JSON.stringify({{
deferred, unavailable, writes, changes,
later:saved.later.map(item => [item.notification_id,item.deferred_until]),
unchanged:beforeFailure === JSON.stringify(store.read()),
}}));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"deferred": "deferred",
"unavailable": "unavailable",
"writes": 2,
"changes": [
["defer", "update:stackchain/api:17:91", "2026-08-09T09:00:00.000Z"],
["defer", "update:stackchain/web:8:92", "2026-08-09T09:00:00.000Z"],
],
"later": [
[91, "2026-08-09T09:00:00.000Z"],
[92, "2026-08-09T09:00:00.000Z"],
],
"unchanged": True,
}
def test_later_queue_prunes_missing_work_and_wakes_expired_items_without_reload():
script = f"""
const createLaterWork = require({json.dumps(str(LATER_WORK))});
@@ -5582,6 +5635,20 @@ async def test_updates_view_offers_accessible_sticky_mobile_subset_selection():
assert "body: JSON.stringify({ ids })" in html
@pytest.mark.anyio
async def test_selected_updates_can_be_deferred_together_without_marking_them_read():
html = await dashboard()
assert 'id="defer-selected-today"' in html
assert 'id="defer-selected-tomorrow"' in html
assert 'laterWork.deferMany(selectedUpdates, until)' in html
assert "notificationSelection.cancel()" in html
assert "Selection unchanged." in html
assert "work stays unread and unchanged in Gitea" in html
assert '.my-work-bulk-actions { display:grid;' in html
assert '.my-work-bulk-actions button { min-height:44px; width:100%; }' in html
@pytest.mark.anyio
async def test_updates_view_discloses_incomplete_inbox_and_loads_more_on_mobile():
html = await dashboard()