feat: review watched pull request changes (Closes #1328)
CI / lint (pull_request) Successful in 3m22s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Failing after 5m6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-24 00:40:28 +00:00
1 parent e39e92b025
commit df90fb1e0e
22 files changed
+344 -45

No files matched your search

@@ -58,6 +58,29 @@ def test_following_queue_is_phone_usable_at_narrow_viewport(viewport):
assert page.locator(".following-card").bounding_box()["height"] >= 44
assert page.locator("#close-following").bounding_box()["height"] >= 44
page.evaluate("""() => {
document.querySelector('#following-sheet').close();
document.querySelector('#search-preview').classList.add('open');
renderSearchPreviewReview({status:'ready',data:{ci_state:'success',files:[{
filename:'src/a/very/long/mobile/path/that-must-wrap/review.py',status:'modified',
additions:1,deletions:1,diff_available:true,
diff_lines:['@@ -1 +1 @@','-unsafe long line that scrolls inside the diff only',
'+safe long line that scrolls inside the diff only']
}]}},document,value=>String(value));
document.querySelector('[data-search-preview-section="changes"]').hidden=false;
}""")
changes = page.locator("#search-preview-review")
expect(changes).to_be_visible()
expect(changes).to_contain_text("CI passed · 1 changed file.")
expect(changes).to_contain_text("review.py")
assert page.locator('[data-search-preview-section="changes"]').bounding_box()["height"] >= 44
assert page.locator("#search-preview-review .pull-diff").evaluate(
"node => node.scrollWidth >= node.clientWidth"
) is True
assert page.evaluate(
"document.documentElement.scrollWidth > document.documentElement.clientWidth"
) is False
retire = page.locator("#watch-search-result")
page.evaluate("""() => {
document.querySelector('#following-sheet').close();
+1 -1
View File
@@ -1334,7 +1334,7 @@ def test_closed_issue_preview_reopens_then_resumes_through_capacity_guard():
html = dashboard_bundle_text()
assert "detail.reopenable ? 'Reopen & resume'" in html
assert "mutate: (detail, action) => fetchReviewJson(" in html
assert "mutate:(detail,action)=>fetchReviewJson(" in html
handler = html.split("qs('#start-search-result').addEventListener('click'", 1)[1].split(
"qs('#close-whiteboard')", 1
)[0]
+1 -1
View File
@@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
assert '.update-reply-actions button { min-height:44px;' in html
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v137" in worker
assert "stackchain-dashboard-shell-v138" in worker
+57
View File
@@ -103,6 +103,63 @@ async def test_confirmed_pull_watch_round_trips_through_following(monkeypatch, t
]
@pytest.mark.anyio
async def test_watched_pull_can_load_read_only_review_context_without_assignment(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"r" * 32)
store.set_watching("timmy", {
"repository": "stackchain/api", "kind": "pull", "number": 84,
"title": "Review watched changes", "state": "open",
"updated_at": "2026-08-24T05:00:00Z",
"url": "https://forge.example/stackchain/api/pulls/84",
}, True)
review = {
"head_sha": "abc123", "ci_state": "success",
"files": [{"filename": "src/api.py", "status": "modified", "patch": "+safe"}],
}
async def user():
return {"login": "timmy"}
async def load_review(repository, number):
assert (repository, number) == ("stackchain/api", 84)
return review
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main.gitea_proxy, "pull_completion_review", load_review)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/v1/following/stackchain/api/pulls/84/review-data"
)
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.json() == review
@pytest.mark.anyio
async def test_unwatched_pull_review_context_is_not_exposed(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"u" * 32)
async def user():
return {"login": "timmy"}
async def unexpected_review(*_args):
raise AssertionError("unwatched pull must be rejected before upstream access")
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main.gitea_proxy, "pull_completion_review", unexpected_review)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/v1/following/stackchain/api/pulls/84/review-data"
)
assert response.status_code == 404
@pytest.mark.anyio
async def test_full_following_collection_rejects_watch_before_gitea_mutation(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"b" * 32, limit=1)
+71 -1
View File
@@ -176,6 +176,76 @@ const preview=createSearchPreview({{
}
def test_following_pull_revision_waits_for_changes_and_ci_context():
preview_module = ROOT / "frontend" / "search-preview.js"
script = f"""
const createSearchPreview=require({json.dumps(str(preview_module))});
let unavailable=true;
const opened=[];
const states=[];
const preview=createSearchPreview({{
fetchJson:async item=>({{...item,title:'Changed pull request'}}),
fetchConversation:async()=>({{comments:[]}}),
fetchReview:async()=>{{if(unavailable)throw new Error('diff offline');return {{ci_state:'success',files:[{{filename:'src/api.py',diff_lines:['+safe'],diff_available:true}}]}};}},
onOpened:async item=>opened.push(item.updated_at),
onState:state=>states.push({{status:state.review?.status,ci:state.review?.data?.ci_state}}),
}});
(async()=>{{
await preview.open({{
repository:'stackchain/api',kind:'pull',number:84,following:true,
reviewed_at:'2026-08-23T03:00:00Z',updated_at:'2026-08-23T04:00:00Z'
}});
const afterFailure=[...opened];
unavailable=false;
await preview.retryReview();
process.stdout.write(JSON.stringify({{afterFailure,opened,last:states.at(-1)}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result == {
"afterFailure": [],
"opened": ["2026-08-23T04:00:00Z"],
"last": {"status": "ready", "ci": "success"},
}
def test_watched_pull_changes_render_bounded_diff_and_ci_state():
preview_module = ROOT / "frontend" / "search-preview.js"
script = f"""
require({json.dumps(str(preview_module))});
const nodes={{
'#search-preview-review':{{hidden:true}},
'#search-preview-review-status':{{textContent:''}},
'#search-preview-files':{{innerHTML:''}},
'#retry-search-preview-review':{{hidden:true}},
}};
const document={{querySelector:selector=>nodes[selector]}};
global.renderSearchPreviewReview({{status:'ready',data:{{ci_state:'success',files:[{{
filename:'src/api.py',status:'modified',additions:1,deletions:0,
diff_available:true,diff_lines:['@@ -1 +1 @@','-unsafe','+safe']
}}]}}}},document,value=>String(value).replaceAll('<','&lt;'));
process.stdout.write(JSON.stringify({{
hidden:nodes['#search-preview-review'].hidden,
status:nodes['#search-preview-review-status'].textContent,
html:nodes['#search-preview-files'].innerHTML,
retry:nodes['#retry-search-preview-review'].hidden,
}}));
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result["hidden"] is False
assert result["status"] == "CI passed · 1 changed file."
assert "src/api.py" in result["html"]
assert "pull-diff-line added" in result["html"]
assert result["retry"] is True
def test_following_typed_identity_prevents_issue_pull_collisions():
script = f"""
const createFollowing = require({json.dumps(str(MODULE))});
@@ -523,7 +593,7 @@ process.stdout.write(JSON.stringify({{
assert ".following-disposition-mode" in css
assert "if (searchPreviewReturnKind === 'following')" in dashboard
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
assert "stackchain-dashboard-shell-v137" in service_worker
assert "stackchain-dashboard-shell-v138" in service_worker
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
+1 -1
View File
@@ -435,5 +435,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-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -256,4 +256,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-v137" in worker
assert "stackchain-dashboard-shell-v138" in worker
+1 -1
View File
@@ -45,7 +45,7 @@ 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-v137" in worker
assert "stackchain-dashboard-shell-v138" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "controller.recoverPermission('deadline')" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
assert "stackchain-dashboard-shell-v137" in worker
assert "stackchain-dashboard-shell-v138" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -243,5 +243,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
def test_mobile_insights_rolls_into_the_offline_shell():
worker = (CONTROLLER.parent / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v137" in worker
assert "stackchain-dashboard-shell-v138" in worker
assert "BASE + 'static/mobile-insights.js'" in worker
+1 -1
View File
@@ -414,7 +414,7 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
assert ".mobile-start-day-finish { min-height:44px;" in html
assert "max-width:100%; overflow-wrap:anywhere;" in html
assert "BASE + 'static/mobile-start-day.js'" in service_worker
assert "stackchain-dashboard-shell-v137" in service_worker
assert "stackchain-dashboard-shell-v138" in service_worker
@pytest.mark.anyio
+1 -1
View File
@@ -418,7 +418,7 @@ async def test_starting_saved_today_work_closes_a_concurrent_rollover_planner():
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-readiness.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source
+16 -16
View File
@@ -189,7 +189,7 @@ async function dispatchPush(payload) {{
def test_week_unplan_undo_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/week-plan.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -197,20 +197,20 @@ def test_week_unplan_undo_rolls_the_offline_shell():
def test_private_today_action_mailbox_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/week-plan.js'" in source
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -219,7 +219,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/authored-outbox.js'" in source
assert "BASE + 'static/background-issue-sync.js'" in source
@@ -228,7 +228,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/issue-evidence-review.js'" in source
assert "BASE + 'static/issue-attachment.js'" in source
@@ -236,14 +236,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/dashboard.js'" in source
def test_offline_review_next_ships_today_completion_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -251,7 +251,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-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -259,7 +259,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/issue-sheet.js'" in source
assert "BASE + 'static/checklist-conflict.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -269,14 +269,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v137" in source
assert "stackchain-dashboard-shell-v138" 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-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -285,21 +285,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-v137" in source
assert "stackchain-dashboard-shell-v138" 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-v137" in source
assert "stackchain-dashboard-shell-v138" 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-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -1354,7 +1354,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-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/queue-today.js'" in source
+1 -1
View File
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
def test_readiness_runtime_is_available_in_offline_shell():
service_worker = SERVICE_WORKER.read_text()
assert "const CACHE = 'stackchain-dashboard-shell-v137';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v138';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete']();
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-v137" in source
assert "stackchain-dashboard-shell-v138" in source
assert "BASE + 'static/today-sync.js'" in source