feat: surface Today issue blockers (#435)
This commit is contained in:
1 parent
e0de45ea90
commit
05f9e06a10
14 files changed
+270
-23
No files matched your search
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user