feat: start work from unread updates (Closes #505)
CI / lint (pull_request) Successful in 1m9s
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-10 18:29:03 +00:00
1 parent b26b57d616
commit 2717f41957
6 files changed
+237 -14

No files matched your search

+118
View File
@@ -176,19 +176,137 @@ controller.open({{repository:'stackchain/api', issue:{{number:7, claimable:true}
}
def test_unread_issue_update_claims_and_starts_once_without_marking_it_read():
script = f"""
const createUpdateOwnership = require({json.dumps(str(UPDATE_OWNERSHIP))});
let claims = 0;
let starts = 0;
let finishClaim;
const reconciled = [];
const states = [];
const controller = createUpdateOwnership({{
available: () => true,
claim: () => {{ claims += 1; return new Promise(resolve => {{ finishClaim = resolve; }}); }},
addToday: () => 'added',
start: item => {{ starts += 1; return item.notification_id === 42 ? 'started' : 'broken'; }},
recover: () => {{ throw new Error('recovery should not run'); }},
onClaimed: item => reconciled.push(item),
onStartState: state => states.push(state),
onState: () => {{}},
}});
controller.open({{
repository:'stackchain/api', title:'Retry deploy',
issue:{{number:7, assignees:[], claimable:true}},
}}, {{notification_id:42, updated_at:'2026-08-08T12:00:00Z'}});
const first = controller.start();
const second = controller.start();
if (first !== second || claims !== 1) throw new Error('start was not single-flight');
finishClaim({{number:7, title:'Retry deploy', assignees:['timmy'], state:'open'}});
(async () => {{
const result = await first;
process.stdout.write(JSON.stringify({{result, claims, starts, reconciled, states}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["result"] == "started"
assert output["claims"] == 1
assert output["starts"] == 1
assert len(output["reconciled"]) == 1
assert output["reconciled"][0]["notification_id"] == 42
assert output["reconciled"][0]["has_update"] is True
assert output["states"][-1] == {
"action": "hidden",
"busy": False,
"message": "Assigned, added to Today, and ready to work. The update is still unread.",
}
def test_unread_update_start_checks_today_capacity_before_claiming():
script = f"""
const createUpdateOwnership = require({json.dumps(str(UPDATE_OWNERSHIP))});
let claims = 0;
const states = [];
const controller = createUpdateOwnership({{
available: () => false,
claim: () => {{ claims += 1; return Promise.resolve({{number:7}}); }},
addToday: () => 'added', start: () => 'started',
onStartState: state => states.push(state), onState: () => {{}},
}});
controller.open({{repository:'stackchain/api', issue:{{number:7, claimable:true}}}}, {{notification_id:42}});
(async () => {{
const result = await controller.start();
process.stdout.write(JSON.stringify({{result, claims, states}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["result"] == "full"
assert output["claims"] == 0
assert output["states"][-1] == {
"action": "start",
"busy": False,
"message": "Today is limited to 5 items. Remove one before taking ownership.",
}
def test_unread_update_opens_owned_issue_when_today_start_throws():
script = f"""
const createUpdateOwnership = require({json.dumps(str(UPDATE_OWNERSHIP))});
let recovered = null;
const states = [];
const controller = createUpdateOwnership({{
available: () => true,
claim: () => Promise.resolve({{number:7, title:'Retry deploy'}}),
addToday: () => 'added',
start: () => {{ throw new Error('storage failed'); }},
recover: item => {{ recovered = item; }},
onStartState: state => states.push(state), onState: () => {{}},
}});
controller.open({{repository:'stackchain/api', issue:{{number:7, claimable:true}}}}, {{notification_id:42}});
(async () => {{
const result = await controller.start();
process.stdout.write(JSON.stringify({{result, recovered, states}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["result"] == "recovery"
assert output["recovered"]["number"] == 7
assert output["states"][-1] == {
"action": "hidden",
"busy": False,
"message": "Assigned to you, but Today could not start. The update is still unread; the owned issue is open so you can recover.",
}
@pytest.mark.anyio
async def test_mobile_update_sheet_wires_phone_safe_ownership_to_my_work_and_today():
html = await dashboard()
assert '<script src="static/update-ownership.js"></script>' in html
assert 'id="update-ownership-action"' in html
assert 'id="update-ownership-start"' in html
assert '>Take ownership &amp; start</button>' in html
assert 'hidden aria-describedby="update-sheet-status"' in html
assert 'const updateOwnership = createUpdateOwnership({' in html
assert 'available: () => createAndStart.available()' in html
assert 'const outcome = createAndStart.complete(item)' in html
assert "openRoutedWork(item, qs('#update-ownership-start'))" in html
assert 'updateOwnership.open(detail, selectedUpdate)' in html
assert "qs('#update-ownership-action').addEventListener('click'" in html
assert "qs('#update-ownership-start').addEventListener('click', () => updateOwnership.start())" in html
assert "lastContextSnapshot.issues" in html
assert "todayWork.add(item)" in html
assert '.update-sheet-actions button, .update-sheet-actions a { min-height:44px;' in html
assert '.update-ownership-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
assert '.update-sheet-panel { width:100%; border-left:0; padding:14px; overflow-x:hidden;' in html
def test_work_routes_round_trip_all_sheet_kinds_and_reject_unsafe_fragments():