feat: surface Today issue blockers (#435)
CI / lint (pull_request) Successful in 53s
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 00:00:51 +00:00
1 parent e0de45ea90
commit 05f9e06a10
14 files changed
+270 -23

No files matched your search

+55
View File
@@ -1543,6 +1543,25 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
async def handler(request):
requests.append((request.method, request.url.path, request.url.query.decode()))
if request.url.path.endswith("/dependencies"):
return httpx.Response(
200,
json=[
{
"number": 3,
"title": "Restore signing service",
"state": "open",
"html_url": "https://forge.example/stackchain/platform/issues/3",
"repository": {"full_name": "stackchain/platform"},
},
{
"number": 2,
"title": "Completed prerequisite",
"state": "closed",
"repository": {"full_name": "stackchain/platform"},
},
],
)
if request.url.path.endswith("/comments"):
return httpx.Response(
200,
@@ -1580,6 +1599,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
assert requests == [
("GET", "/api/v1/repos/stackchain/api/issues/7", ""),
("GET", "/api/v1/repos/stackchain/api/issues/7/comments", "limit=20&page=1"),
("GET", "/api/v1/repos/stackchain/api/issues/7/dependencies", "limit=20"),
]
assert result == {
"repository": "stackchain/api",
@@ -1593,6 +1613,16 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
"url": "https://forge.example/stackchain/api/issues/7",
"labels": ["P1"],
"assignees": ["timmy"],
"dependencies_available": True,
"dependencies": [
{
"repository": "stackchain/platform",
"number": 3,
"title": "Restore signing service",
"state": "open",
"url": "https://forge.example/stackchain/platform/issues/3",
}
],
"comments": [
{
"id": 81,
@@ -1619,6 +1649,31 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
}
@pytest.mark.anyio
async def test_gitea_issue_detail_keeps_planning_usable_when_dependencies_are_unavailable():
async def handler(request):
if request.url.path.endswith("/dependencies"):
return httpx.Response(503, json={"message": "upstream timeout"})
if request.url.path.endswith("/comments"):
return httpx.Response(200, json=[])
return httpx.Response(200, json={
"number": 7,
"title": "Fix mobile flow",
"state": "open",
"assignees": [{"login": "timmy"}],
})
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
result = await gitea_proxy.issue_detail("stackchain/api", 7)
finally:
await gitea_proxy.stop_client()
assert result["title"] == "Fix mobile flow"
assert result["dependencies_available"] is False
assert result["dependencies"] == []
@pytest.mark.anyio
async def test_gitea_conversation_page_opens_newest_page_and_reports_older_history():
requests = []
+1 -1
View File
@@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v72" in worker
assert "stackchain-dashboard-shell-v73" in worker
+1 -1
View File
@@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v72" in worker
assert "stackchain-dashboard-shell-v73" in worker
+65 -1
View File
@@ -126,6 +126,64 @@ process.stdout.write(JSON.stringify({{first, second, scroll, events, planner:pla
}
def test_plan_today_preview_requires_explicit_override_for_blocked_or_unknown_issue():
script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const planner = createPlanToday({{
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
save: () => true,
}});
planner.open([], [item(2), item(3)]);
const preview = createPlanTodayPreview({{
planner,
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
}});
preview.open(item(2));
preview.setDependencies({{available:true, dependencies:[{{repository:'stackchain/api', number:9, title:'Restore API', state:'open'}}]}});
const blockedState = preview.snapshot();
const blocked = preview.close({{add:true}});
const overridden = preview.close({{add:true, override:true}});
preview.open(item(3));
preview.setDependencies({{available:false, dependencies:[]}});
const unavailableState = preview.snapshot();
const unavailable = preview.close({{add:true}});
const unavailableOverride = preview.close({{add:true, override:true}});
process.stdout.write(JSON.stringify({{blockedState, blocked, overridden, unavailableState, unavailable, unavailableOverride, planner:planner.snapshot()}}));
"""
assert run_node(script) == {
"blockedState": {
"open": True,
"item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 2, "title": "Issue 2"},
"trigger": None,
"scroll": 0,
"dependencies_available": True,
"dependencies": [{"repository": "stackchain/api", "number": 9, "title": "Restore API", "state": "open"}],
"requires_override": True,
},
"blocked": "blocked",
"overridden": "added",
"unavailableState": {
"open": True,
"item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 3, "title": "Issue 3"},
"trigger": None,
"scroll": 0,
"dependencies_available": False,
"dependencies": [],
"requires_override": True,
},
"unavailable": "dependencies-unavailable",
"unavailableOverride": "added",
"planner": {
"open": True,
"ids": ["issue:stackchain/dashboard:2:", "issue:stackchain/dashboard:3:"],
"count": 2,
"limit": 5,
},
}
@pytest.mark.anyio
async def test_mobile_dashboard_wires_focused_plan_today_sheet():
html = await dashboard()
@@ -150,6 +208,12 @@ async def test_mobile_dashboard_wires_focused_plan_today_sheet():
assert 'id="add-plan-preview"' in html
assert "taskOverlayHistory.open('plan-today-preview')" in html
assert "planTodayPreview.close({ add:true })" in html
assert 'id="issue-blockers"' in html
assert "planTodayPreview.setDependencies({" in html
assert "Add blocked item anyway & back" in html
assert "Blocker status unavailable" in html
assert ".issue-blockers { max-width:100%; overflow-x:hidden;" in html
assert ".issue-blocker { min-width:0; overflow-wrap:anywhere;" in html
assert "Preview unavailable offline" in html
@@ -168,6 +232,6 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source
+9 -9
View File
@@ -122,7 +122,7 @@ async function dispatchNotificationClick(route) {{
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -131,7 +131,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_offline_review_next_ships_today_completion_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -139,7 +139,7 @@ def test_offline_review_next_ships_today_completion_atomically():
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -147,14 +147,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -163,21 +163,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -358,7 +358,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
def test_queue_today_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/queue-today.js'" in source
+1 -1
View File
@@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v72" in source
assert "stackchain-dashboard-shell-v73" in source
assert "BASE + 'static/today-sync.js'" in source