feat: schedule search results into Later (Closes #799)
CI / lint (pull_request) Successful in 1m42s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 04:30:02 +00:00
1 parent 0a8f33dc83
commit 71b38687c4
8 files changed
+167 -1

No files matched your search

+95
View File
@@ -11,6 +11,7 @@ FRONTEND = Path(__file__).parents[1] / "frontend"
COMMANDS = FRONTEND / "commands.js"
SEARCH_PREVIEW = FRONTEND / "search-preview.js"
MOBILE_SEARCH_VIEWPORT = FRONTEND / "mobile-search-viewport.js"
SEARCH_DEFER = FRONTEND / "search-defer.js"
class ScriptSourceParser(HTMLParser):
@@ -552,6 +553,100 @@ def test_search_queue_confirmation_is_accessible_and_existing_today_item_is_not_
assert handler.index("todayWork.contains(detail)") < handler.index("searchAssignAndStart.run")
def test_search_defer_claims_once_then_schedules_the_canonical_issue_without_touching_today():
script = f"""
const createSearchDefer = require({json.dumps(str(SEARCH_DEFER))});
(async () => {{
const calls = [];
let releaseClaim;
const flow = createSearchDefer({{
claim: detail => {{
calls.push(['claim', detail.number]);
return new Promise(resolve => releaseClaim = resolve);
}},
accept: confirmed => {{
calls.push(['accept', confirmed.key]);
return {{kind:'issue', repository:confirmed.repository, number:confirmed.number, key:confirmed.key}};
}},
defer: (item, until) => {{ calls.push(['defer', item.key, until.toISOString()]); return 'deferred'; }},
refresh: () => calls.push(['refresh']),
announce: message => calls.push(['announce', message]),
formatTime: value => value.toISOString(),
}});
const detail = {{kind:'issue', repository:'stackchain/api', number:42, claimable:true}};
const until = new Date('2026-08-15T09:00:00.000Z');
const first = flow.run(detail, until);
const second = flow.run(detail, until);
if (first !== second) throw new Error('confirmation was not single-flight');
releaseClaim({{kind:'issue', repository:'stackchain/api', number:42, key:'stackchain/api#42'}});
const outcome = await first;
process.stdout.write(JSON.stringify({{outcome, calls}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"outcome": "deferred",
"calls": [
["claim", 42],
["accept", "stackchain/api#42"],
["defer", "stackchain/api#42", "2026-08-15T09:00:00.000Z"],
["refresh"],
["announce", "Assigned and deferred until 2026-08-15T09:00:00.000Z."],
],
}
def test_search_preview_opens_later_picker_before_claiming_an_eligible_issue():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
assert 'id="defer-search-result"' in html
assert "const deferButton = qs('#defer-search-result')" in html
assert "deferButton.hidden = !(detail.kind === 'issue'" in html
assert "detail.assigned_to_me ? 'Defer' : 'Assign & defer'" in html
handler = html.split("qs('#defer-search-result').addEventListener('click'", 1)[1].split(
"qs('#queue-search-result')", 1
)[0]
assert "laterPicker.open(detail, event.currentTarget, 'search')" in handler
assert "searchPreview.claim" not in handler
assert ".search-preview-actions button, .search-preview-actions a { min-height:44px;" in css
assert "@media (max-width:420px)" in css
assert ".search-preview-primary-actions { grid-template-columns:1fr; }" in css
def test_search_defer_reports_recoverable_partial_outcome_when_later_storage_fails_after_claim():
script = f"""
const createSearchDefer = require({json.dumps(str(SEARCH_DEFER))});
(async () => {{
const messages = [];
let refreshes = 0;
const flow = createSearchDefer({{
claim: () => Promise.resolve({{kind:'issue', repository:'stackchain/api', number:42}}),
accept: issue => issue,
defer: () => 'unavailable',
refresh: () => refreshes += 1,
announce: message => messages.push(message),
formatTime: value => value.toISOString(),
}});
const outcome = await flow.run({{kind:'issue', repository:'stackchain/api', number:42, claimable:true}},
new Date('2026-08-15T09:00:00.000Z'));
process.stdout.write(JSON.stringify({{outcome, refreshes, messages}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"outcome": "unavailable",
"refreshes": 1,
"messages": [
"Assignment succeeded, but Later could not be saved. Open this issue in My Work to retry."
],
}
def test_closed_issue_preview_reopens_then_resumes_through_capacity_guard():
html = dashboard_bundle_text()
+1
View File
@@ -788,6 +788,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/markdown.js",
"/dashboard/static/commands.js",
"/dashboard/static/search-preview.js",
"/dashboard/static/search-defer.js",
"/dashboard/static/widgets.js",
"/dashboard/static/drafts.js",
"/dashboard/static/unfiled-captures.js",