feat: draft requested reviews offline (#355)
This commit is contained in:
1 parent
34029e624f
commit
229d112297
9 files changed
+146
-29
No files matched your search
@@ -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-v43" in worker
|
||||
assert "stackchain-dashboard-shell-v44" 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-v43" in worker
|
||||
assert "stackchain-dashboard-shell-v44" in worker
|
||||
@@ -111,6 +111,32 @@ process.stdout.write(JSON.stringify({calls, status}));
|
||||
assert result["status"] == {"total": 2, "ready": 2, "failed": 0, "pending": 0}
|
||||
|
||||
|
||||
def test_requested_reviews_are_warmed_as_distinct_today_items():
|
||||
result = run_scenario("""
|
||||
const loaded = [];
|
||||
const saved = [];
|
||||
const review = {
|
||||
kind:'pull', is_review:true, repository:'stackchain/dashboard', number:12,
|
||||
updated_at:'2026-08-08T12:00:00Z',
|
||||
};
|
||||
const warmer = createOfflineToday({
|
||||
loadDetail: async item => {
|
||||
loaded.push(item.is_review ? 'review' : item.kind);
|
||||
return {head_sha:'abc123', files:[{filename:'src/app.js'}]};
|
||||
},
|
||||
loadSavedDetail: () => null,
|
||||
saveDetail: (_login, item, detail) => saved.push({item, detail}),
|
||||
});
|
||||
const status = await warmer.warm('timmy', [review]);
|
||||
process.stdout.write(JSON.stringify({loaded, saved, status}));
|
||||
""")
|
||||
|
||||
assert result["loaded"] == ["review"]
|
||||
assert result["saved"][0]["item"]["is_review"] is True
|
||||
assert result["saved"][0]["detail"]["head_sha"] == "abc123"
|
||||
assert result["status"] == {"total": 1, "ready": 1, "failed": 0, "pending": 0}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_warms_today_after_fresh_snapshot_and_exposes_mobile_retry():
|
||||
html = await dashboard()
|
||||
|
||||
@@ -154,6 +154,48 @@ process.stdout.write(JSON.stringify({
|
||||
assert "private diff" not in result["raw"]
|
||||
|
||||
|
||||
def test_requested_review_cache_is_sha_scoped_allowlisted_and_bounded():
|
||||
result = run_scenario("""
|
||||
const store = createOfflineWorkStore({storage, now:() => new Date('2026-08-07T12:00:00Z')});
|
||||
store.setEnabled(true);
|
||||
const review = {kind:'pull', repository:'stackchain/dashboard', number:12, is_review:true};
|
||||
store.saveDetail('timmy', review, {
|
||||
title:'Review offline work', body:'Review description', author:'alexander', head_sha:'abc123',
|
||||
ci_state:'success', token:'top-secret', source_updated_at:'2026-08-07T11:00:00Z',
|
||||
files:Array.from({length:55}, (_, fileIndex) => ({
|
||||
filename:'src/file-' + fileIndex + '.js', status:'modified', additions:2, deletions:1,
|
||||
diff_available:true, diff_binary:false, diff_truncated:false, patch:'must-not-persist',
|
||||
diff_lines:Array.from({length:405}, (_, lineIndex) => '+' + fileIndex + ':' + lineIndex),
|
||||
})),
|
||||
reviews:Array.from({length:25}, (_, index) => ({
|
||||
id:index + 1, state:'COMMENT', body:'Review ' + (index + 1),
|
||||
submitted_at:'2026-08-07T10:00:00Z', user:{login:'reviewer-' + index, email:'private@example.test'},
|
||||
token:'review-secret',
|
||||
})),
|
||||
});
|
||||
const loaded = store.loadDetail('timmy', review);
|
||||
process.stdout.write(JSON.stringify({
|
||||
loaded,
|
||||
wrongUser:store.loadDetail('alexander', review),
|
||||
raw:[...values.values()].join(' '),
|
||||
}));
|
||||
""")
|
||||
|
||||
assert result["wrongUser"] is None
|
||||
assert result["loaded"]["head_sha"] == "abc123"
|
||||
assert result["loaded"]["ci_state"] == "success"
|
||||
assert len(result["loaded"]["files"]) == 50
|
||||
assert len(result["loaded"]["files"][0]["diff_lines"]) == 400
|
||||
assert result["loaded"]["files"][0]["filename"] == "src/file-0.js"
|
||||
assert len(result["loaded"]["reviews"]) == 20
|
||||
assert result["loaded"]["reviews"][0]["body"] == "Review 6"
|
||||
assert result["loaded"]["reviews"][0]["user"] == {"login": "reviewer-5"}
|
||||
assert "top-secret" not in result["raw"]
|
||||
assert "must-not-persist" not in result["raw"]
|
||||
assert "private@example.test" not in result["raw"]
|
||||
assert "review-secret" not in result["raw"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_offers_private_offline_work_controls_and_read_only_hydration():
|
||||
html = await dashboard()
|
||||
@@ -219,3 +261,17 @@ async def test_saved_unread_update_opens_offline_with_queued_reply_and_read_cont
|
||||
assert "qs('#load-older-update-comments').disabled = offline;" in html
|
||||
assert "document.querySelectorAll('[data-notification-id], [data-later-preset], [data-today-add]')" in html
|
||||
assert "replies and read acknowledgements queue for sync" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_saved_requested_review_opens_offline_for_sha_scoped_drafting_only():
|
||||
html = await dashboard()
|
||||
|
||||
assert "item.is_review ? reviewController.load(item)" in html
|
||||
assert "openReviewSheet(item, trigger, savedDetail)" in html
|
||||
assert "async function openReviewSheet(item, trigger, cachedDetail = null)" in html
|
||||
assert "cachedDetail || await reviewController.load(selectedReview)" in html
|
||||
assert "Offline review · saved " in html
|
||||
assert "Reconnect to validate & submit" in html
|
||||
assert "qs('#submit-review').disabled = offlineReview;" in html
|
||||
assert "if (selectedReview && offlineReview) openReviewSheet(selectedReview, reviewTrigger);" in html
|
||||
@@ -108,7 +108,7 @@ async function dispatchNotificationClick(route) {{
|
||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v43" in source
|
||||
assert "stackchain-dashboard-shell-v44" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -117,14 +117,14 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v43" in source
|
||||
assert "stackchain-dashboard-shell-v44" 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-v43" in source
|
||||
assert "stackchain-dashboard-shell-v44" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user