feat: warn before duplicate mobile issue capture (#371)
This commit is contained in:
1 parent
1bf759dc76
commit
c0fc4eddd0
12 files changed
+293
-21
No files matched your search
@@ -233,5 +233,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-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" 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-v51" in worker
|
||||
assert "stackchain-dashboard-shell-v52" 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-v51" in worker
|
||||
assert "stackchain-dashboard-shell-v52" in worker
|
||||
@@ -1966,6 +1966,113 @@ Promise.all([capture.loadMilestones('stackchain/api'), capture.submit(capture.lo
|
||||
assert output["stored"]["number"] == 221
|
||||
|
||||
|
||||
def test_issue_capture_finds_only_open_issue_duplicates_in_the_selected_repository():
|
||||
script = f"""
|
||||
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
||||
const calls = [];
|
||||
const capture = createIssueCapture({{
|
||||
storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}},
|
||||
fetchJson: url => {{
|
||||
calls.push(url);
|
||||
return Promise.resolve({{items:[
|
||||
{{kind:'issue', state:'open', repository:'stackchain/api', number:7, title:'Investigate checkout latency'}},
|
||||
{{kind:'pull', state:'open', repository:'stackchain/api', number:8, title:'Investigate checkout latency'}},
|
||||
{{kind:'issue', state:'closed', repository:'stackchain/api', number:9, title:'Investigate checkout latency'}},
|
||||
{{kind:'issue', state:'open', repository:'stackchain/web', number:10, title:'Investigate checkout latency'}},
|
||||
{{kind:'issue', state:'open', repository:'stackchain/api', number:11, title:'Checkout latency alert'}},
|
||||
{{kind:'issue', state:'open', repository:'stackchain/api', number:12, title:'Checkout latency follow-up'}},
|
||||
{{kind:'issue', state:'open', repository:'stackchain/api', number:13, title:'Fourth match is bounded'}},
|
||||
]}});
|
||||
}},
|
||||
}});
|
||||
capture.findDuplicates({{repository:'stackchain/api', title:' Investigate checkout latency '}}).then(state =>
|
||||
process.stdout.write(JSON.stringify({{calls, state}}))
|
||||
);
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
output = json.loads(result.stdout)
|
||||
|
||||
assert output["calls"] == [
|
||||
"api/v1/search?q=Investigate%20checkout%20latency&limit=10"
|
||||
]
|
||||
assert output["state"]["status"] == "ready"
|
||||
assert [item["number"] for item in output["state"]["candidates"]] == [7, 11, 12]
|
||||
|
||||
|
||||
def test_issue_capture_ignores_stale_duplicate_results_and_resets_create_anyway_acknowledgement():
|
||||
script = f"""
|
||||
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
||||
const pending = [];
|
||||
const capture = createIssueCapture({{
|
||||
storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}},
|
||||
fetchJson: url => new Promise((resolve, reject) => pending.push({{url, resolve, reject}})),
|
||||
}});
|
||||
const firstDraft = {{repository:'stackchain/api', title:'Checkout latency'}};
|
||||
const latestDraft = {{repository:'stackchain/api', title:'Checkout latency alert'}};
|
||||
const first = capture.findDuplicates(firstDraft);
|
||||
const latest = capture.findDuplicates(latestDraft);
|
||||
pending[1].resolve({{items:[{{kind:'issue',state:'open',repository:'stackchain/api',number:22,title:'Checkout latency alert'}}]}});
|
||||
latest.then(latestState => {{
|
||||
const blocked = capture.needsDuplicateAcknowledgement(latestDraft);
|
||||
capture.acknowledgeDuplicates(latestDraft);
|
||||
const allowed = capture.needsDuplicateAcknowledgement(latestDraft);
|
||||
const changedDraft = {{...latestDraft,title:'Checkout latency alert today'}};
|
||||
const changedSearch = capture.findDuplicates(changedDraft);
|
||||
pending[2].resolve({{items:[{{kind:'issue',state:'open',repository:'stackchain/api',number:23,title:'Checkout latency alert today'}}]}});
|
||||
changedSearch.then(() => {{
|
||||
const changed = capture.needsDuplicateAcknowledgement(changedDraft);
|
||||
pending[0].resolve({{items:[{{kind:'issue',state:'open',repository:'stackchain/api',number:21,title:'Old result'}}]}});
|
||||
first.then(staleState => {{
|
||||
const failure = capture.findDuplicates({{repository:'stackchain/api',title:'Network failure'}});
|
||||
pending[3].reject(new Error('offline'));
|
||||
failure.then(failedState => process.stdout.write(JSON.stringify({{
|
||||
latestState, staleState, blocked, allowed, changed, failedState,
|
||||
failureBlocks:capture.needsDuplicateAcknowledgement({{repository:'stackchain/api',title:'Network failure'}}),
|
||||
}})));
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
output = json.loads(result.stdout)
|
||||
|
||||
assert output["latestState"]["status"] == "ready"
|
||||
assert output["staleState"]["status"] == "stale"
|
||||
assert output["blocked"] is True
|
||||
assert output["allowed"] is False
|
||||
assert output["changed"] is True
|
||||
assert output["failedState"]["status"] == "failed"
|
||||
assert output["failureBlocks"] is False
|
||||
|
||||
|
||||
def test_issue_capture_keeps_partial_duplicate_search_advisory():
|
||||
script = f"""
|
||||
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
||||
const draft = {{repository:'stackchain/api', title:'Checkout latency'}};
|
||||
const capture = createIssueCapture({{
|
||||
storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}},
|
||||
fetchJson: () => Promise.resolve({{partial:true, items:[
|
||||
{{kind:'issue',state:'open',repository:'stackchain/api',number:24,title:'Checkout latency'}},
|
||||
]}}),
|
||||
}});
|
||||
capture.findDuplicates(draft).then(state => process.stdout.write(JSON.stringify({{
|
||||
state, blocked:capture.needsDuplicateAcknowledgement(draft),
|
||||
}})));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
output = json.loads(result.stdout)
|
||||
|
||||
assert output["state"]["partial"] is True
|
||||
assert [item["number"] for item in output["state"]["candidates"]] == [24]
|
||||
assert output["blocked"] is False
|
||||
|
||||
|
||||
def test_unread_updates_enrich_matching_work_and_keep_unassigned_mentions_actionable():
|
||||
payload = {
|
||||
"user": {"login": "timmy"},
|
||||
@@ -3148,6 +3255,25 @@ async def test_mobile_my_work_captures_new_issue_in_accessible_draft_safe_sheet(
|
||||
assert '.create-issue-label-option' in html and 'min-height:44px' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_issue_capture_warns_before_queuing_a_possible_duplicate():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="create-issue-duplicates"' in html
|
||||
assert 'id="create-issue-duplicate-list"' in html
|
||||
assert 'aria-live="polite"' in html
|
||||
assert 'id="create-issue-anyway"' in html and 'Create anyway' in html
|
||||
assert "issueCapture.findDuplicates(draft)" in html
|
||||
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in html
|
||||
assert "issueCapture.acknowledgeDuplicates(captureDraft)" in html
|
||||
assert html.index("issueCapture.needsDuplicateAcknowledgement(captureDraft)") < html.index(
|
||||
"issueOutbox.enqueueDurably(captureDraft)"
|
||||
)
|
||||
assert ".create-issue-duplicate-card" in html
|
||||
assert ".create-issue-duplicate-card a" in html and "min-height:44px" in html
|
||||
assert "overflow-wrap:anywhere" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_dashboard_puts_filterable_my_work_before_auxiliary_panels():
|
||||
html = await dashboard()
|
||||
|
||||
@@ -105,17 +105,25 @@ async function dispatchNotificationClick(route) {{
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v52" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" 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-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -124,21 +132,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-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" 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-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" 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-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,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-v51" in source
|
||||
assert "stackchain-dashboard-shell-v52" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user