From d54f58bf3670923c194b308de39e1f4d424b3d3d Mon Sep 17 00:00:00 2001 From: timmy Date: Thu, 13 Aug 2026 21:24:34 +0000 Subject: [PATCH] feat: prioritize actionable update triage (Closes #773) --- frontend/dashboard.js | 3 +- frontend/my-work.js | 20 +++++++++- frontend/update-triage-session.js | 14 ++++++- tests/test_my_work.py | 60 +++++++++++++++++++++++++++-- tests/test_update_triage_session.py | 24 ++++++++++++ 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/frontend/dashboard.js b/frontend/dashboard.js index b17f9b4..615490f 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -1047,7 +1047,8 @@ onProgress: state => { const progress = qs('#update-triage-progress'); progress.hidden = false; - progress.textContent = 'Update ' + state.index + ' of ' + state.total; + progress.textContent = 'Update ' + state.index + ' of ' + state.total + + (state.reason ? ' ยท ' + state.reason : ''); }, onFinish: outcome => { notificationReader.prefetch(); diff --git a/frontend/my-work.js b/frontend/my-work.js index 5c0f4b0..e89c5b2 100644 --- a/frontend/my-work.js +++ b/frontend/my-work.js @@ -44,6 +44,19 @@ function needsAttention(item) { return Boolean(item && (item.has_update || item.is_review || urgencyReason(item))); } +function updateActionability(item) { + const priorityLabels = ['p0', 'priority-high', 'critical']; + if ((item?.labels || []).some(label => priorityLabels.includes(String(label).toLowerCase()))) { + return { priority: 0, reason: 'Critical' }; + } + if (item?.is_review) return { priority: 1, reason: 'Review requested' }; + if (item?.is_assigned && ['Overdue', 'Due today'].includes(item?.due_label)) { + return { priority: 2, reason: item.due_label }; + } + if (item?.is_assigned) return { priority: 3, reason: 'Assigned to you' }; + return { priority: 4, reason: '' }; +} + function buildMyWork(data, now = new Date()) { const login = data.user?.login || ''; const issues = (data.issues || []).map((item) => ({ ...item, kind: 'issue' })); @@ -82,13 +95,15 @@ function buildMyWork(data, now = new Date()) { const subjectKind = String(update.subject_type || '').toLowerCase().includes('pull') ? 'pull' : 'issue'; const existing = byKey.get(subjectKind + ':' + key); if (existing) { + const actionability = updateActionability(existing); existing.has_update = true; existing.needs_attention = true; existing.attention_reason = 'Unread update'; existing.notification_id = update.id; existing.url = update.url || existing.url; existing.updated_at = update.updated_at || existing.updated_at; - existing._priority = Math.min(existing._priority, 1); + existing.update_reason = actionability.reason; + existing._priority = actionability.priority; return; } if (!update.url) return; @@ -103,7 +118,8 @@ function buildMyWork(data, now = new Date()) { attention_reason: 'Unread update', notification_id: update.id, reason: 'Unread update', - _priority: 1, + update_reason: '', + _priority: 4, }; work.push(item); byKey.set(subjectKind + ':' + key, item); diff --git a/frontend/update-triage-session.js b/frontend/update-triage-session.js index fba4b65..09be423 100644 --- a/frontend/update-triage-session.js +++ b/frontend/update-triage-session.js @@ -64,7 +64,12 @@ state.current = identity(items[index]); persist(); const originalIndex = state.identities.indexOf(state.current); - options.onProgress({ index:originalIndex + 1, total:state.identities.length }); + const progress = { + index:originalIndex + 1, + total:state.identities.length, + }; + if (items[index]?.update_reason) progress.reason = String(items[index].update_reason); + options.onProgress(progress); options.onOpen(items[index]); return true; } @@ -79,7 +84,12 @@ state.current = identity(next); if (openNext) return openCurrent(); persist(); - options.onProgress({ index:state.identities.indexOf(state.current) + 1, total:state.identities.length }); + const progress = { + index:state.identities.indexOf(state.current) + 1, + total:state.identities.length, + }; + if (next?.update_reason) progress.reason = String(next.update_reason); + options.onProgress(progress); return true; } diff --git a/tests/test_my_work.py b/tests/test_my_work.py index 3fdfc0b..a6a9f19 100644 --- a/tests/test_my_work.py +++ b/tests/test_my_work.py @@ -44,6 +44,56 @@ AGENDA_SESSION_LAUNCHER = Path(__file__).parents[1] / "frontend" / "agenda-sessi UPDATE_TRIAGE_LAUNCHER = Path(__file__).parents[1] / "frontend" / "update-triage-launcher.js" +def test_updates_rank_actionable_work_before_newer_routine_activity(): + payload = { + "user": {"login": "timmy"}, + "issues": [ + {"number": 1, "title": "Critical incident", "repository": "stackchain/api", + "labels": ["P0"], "assignees": [], "updated_at": "2026-08-01T10:00:00Z"}, + {"number": 2, "title": "Overdue assignment", "repository": "stackchain/api", + "labels": [], "assignees": ["timmy"], "due_date": "2026-08-06T23:59:59Z", + "updated_at": "2026-08-05T10:00:00Z"}, + {"number": 3, "title": "Assigned follow-up", "repository": "stackchain/api", + "labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z"}, + ], + "pull_requests": [ + {"number": 4, "title": "Requested review", "repository": "stackchain/web", + "labels": [], "assignees": [], "work_reasons": ["review_requested"], + "updated_at": "2026-08-02T10:00:00Z"}, + ], + "notifications": [ + {"id": 11, "number": 1, "repository": "stackchain/api", "unread": True, + "subject_type": "Issue", "url": "https://forge.example/api/issues/1"}, + {"id": 12, "number": 2, "repository": "stackchain/api", "unread": True, + "subject_type": "Issue", "url": "https://forge.example/api/issues/2"}, + {"id": 13, "number": 3, "repository": "stackchain/api", "unread": True, + "subject_type": "Issue", "url": "https://forge.example/api/issues/3"}, + {"id": 14, "number": 4, "repository": "stackchain/web", "unread": True, + "subject_type": "PullRequest", "url": "https://forge.example/web/pulls/4"}, + {"id": 15, "number": 5, "repository": "stackchain/web", "unread": True, + "subject_type": "Issue", "title": "Newest routine update", + "updated_at": "2026-08-07T11:00:00Z", "url": "https://forge.example/web/issues/5"}, + ], + } + script = f""" +const buildMyWork = require({json.dumps(str(MY_WORK))}); +const updates = buildMyWork({json.dumps(payload)}, new Date('2026-08-07T12:00:00Z')) + .filter(item => item.has_update) + .map(item => ({{title:item.title, update_reason:item.update_reason}})); +process.stdout.write(JSON.stringify(updates)); +""" + result = subprocess.run(["node", "-e", script], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == [ + {"title": "Critical incident", "update_reason": "Critical"}, + {"title": "Requested review", "update_reason": "Review requested"}, + {"title": "Overdue assignment", "update_reason": "Overdue"}, + {"title": "Assigned follow-up", "update_reason": "Assigned to you"}, + {"title": "Newest routine update", "update_reason": ""}, + ] + + def test_update_triage_launch_waits_for_complete_single_flight_discovery(): script = f""" const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))}); @@ -4530,10 +4580,12 @@ process.stdout.write(JSON.stringify({{ output = json.loads(result.stdout) assert len(output["queue"]) == 2 - assert [item["key"] for item in output["updates"]] == ["stackchain/web#8", "stackchain/api#7"] - assert output["updates"][0]["kind"] == "update" - assert output["updates"][1]["kind"] == "issue" - assert output["updates"][1]["url"].endswith("#issuecomment-9") + assert [item["key"] for item in output["updates"]] == ["stackchain/api#7", "stackchain/web#8"] + assert output["updates"][0]["kind"] == "issue" + assert output["updates"][0]["update_reason"] == "Assigned to you" + assert output["updates"][0]["url"].endswith("#issuecomment-9") + assert output["updates"][1]["kind"] == "update" + assert output["updates"][1]["update_reason"] == "" assert output["counts"] == { "all": 2, "attention": 2, "issue": 1, "pull": 0, "review": 0, "update": 2 } diff --git a/tests/test_update_triage_session.py b/tests/test_update_triage_session.py index 128b370..bb43522 100644 --- a/tests/test_update_triage_session.py +++ b/tests/test_update_triage_session.py @@ -17,6 +17,30 @@ def run_session(script): return json.loads(result.stdout) +def test_update_triage_progress_exposes_the_current_actionability_reason(): + result = run_session(""" +const values = new Map(); +const items = [ + {notification_id:7, update_reason:'Review requested'}, + {notification_id:8, update_reason:''}, +]; +const progress = []; +const session = createSession({ + storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, + getLogin:()=> 'timmy', getItems:()=>items, onOpen:()=>{}, + onProgress:value=>progress.push(value), onFinish:()=>{}, +}); +session.start(); +session.completeAndNext(); +process.stdout.write(JSON.stringify(progress)); +""") + + assert result == [ + {"index": 1, "total": 2, "reason": "Review requested"}, + {"index": 2, "total": 2}, + ] + + def test_update_triage_persists_account_bound_pass_and_resumes_by_identity(): result = run_session(""" const values = new Map(); -- 2.43.0