feat: resolve mobile checklist conflicts (Closes #913)
CI / lint (pull_request) Successful in 2m7s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 57s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-15 20:00:14 +00:00
1 parent 7204a426af
commit 7b27f85b39
10 files changed
+281 -6

No files matched your search

+37
View File
@@ -176,6 +176,43 @@ const outbox = createAuthoredOutbox({{
assert output["items"][0]["body"] == "- [x] Build\n- [ ] Test"
def test_issue_content_conflict_resolution_rebases_task_delta_and_mirrors_before_retry():
script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const mergeChecklistConflict = require({json.dumps(str(Path(__file__).parents[1] / 'frontend' / 'checklist-conflict.js'))});
const initial = {{version:2,items:[{{
id:'check-1',operationId:'old-op',kind:'issue-content',ownerLogin:'timmy',status:'attention',
repository:'o/r',number:9,title:'Ship',baseBody:'Intro\\n- [ ] Build\\n- [ ] Test',
body:'Intro\\n- [x] Build\\n- [ ] Test',expectedUpdatedAt:'old-revision',error:'Issue changed',
}}]}};
const values = new Map([['stackchain.authored-outbox.v1', JSON.stringify(initial)]]);
const mirrors = []; let syncs = 0;
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'rebased-op', mergeChecklistConflict,
backgroundSync:{{reconcile:async items=>mirrors.push(items.map(item=>({{...item}}))),requestSync:async()=>{{syncs += 1;}}}},
}});
(async()=>{{
const result = await outbox.resolveIssueContentConflict('check-1', {{
title:'Ship safely', body:'Remote intro\\n- [ ] Test\\n- [ ] Build', updated_at:'latest-revision',
}}, 'timmy');
process.stdout.write(JSON.stringify({{result,item:outbox.list()[0],mirrors,syncs}}));
}})();
"""
output = run_node(script)
assert output["result"]["changes"] == [{"label": "Build", "checked": True}]
assert output["item"]["status"] == "queued"
assert output["item"]["operationId"] == "rebased-op"
assert output["item"]["title"] == "Ship safely"
assert output["item"]["body"] == "Remote intro\n- [ ] Test\n- [x] Build"
assert output["item"]["baseBody"] == "Remote intro\n- [ ] Test\n- [ ] Build"
assert output["item"]["expectedUpdatedAt"] == "latest-revision"
assert "error" not in output["item"]
assert output["mirrors"][-1][0] == output["item"]
assert output["syncs"] == 1
def test_authored_outbox_persists_and_delivers_desired_blocker_state():
script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
+49
View File
@@ -0,0 +1,49 @@
import json
import subprocess
from pathlib import Path
MERGER = Path(__file__).parents[1] / "frontend" / "checklist-conflict.js"
def run_node(script: str):
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
return json.loads(result.stdout)
def test_merge_applies_local_task_delta_to_latest_body_without_reverting_remote_edits():
script = f"""
const mergeChecklistConflict = require({json.dumps(str(MERGER))});
const result = mergeChecklistConflict({{
baseBody:'Intro\\n- [ ] Build\\n- [ ] Test',
localBody:'Intro\\n- [x] Build\\n- [ ] Test',
remoteBody:'Updated intro\\n- [ ] Test\\n- [ ] Build\\n- [x] Document',
}});
process.stdout.write(JSON.stringify(result));
"""
output = run_node(script)
assert output == {
"body": "Updated intro\n- [ ] Test\n- [x] Build\n- [x] Document",
"changes": [{"label": "Build", "checked": True}],
"conflicts": [],
}
def test_merge_reports_missing_or_ambiguous_changed_tasks_without_writing_a_body():
script = f"""
const mergeChecklistConflict = require({json.dumps(str(MERGER))});
const missing = mergeChecklistConflict({{
baseBody:'- [ ] Ship', localBody:'- [x] Ship', remoteBody:'- [ ] Release',
}});
const duplicate = mergeChecklistConflict({{
baseBody:'- [ ] Ship', localBody:'- [x] Ship', remoteBody:'- [ ] Ship\\n- [ ] Ship',
}});
process.stdout.write(JSON.stringify({{missing,duplicate}}));
"""
output = run_node(script)
assert output["missing"]["body"] is None
assert output["missing"]["conflicts"] == [{"label": "Ship", "reason": "missing"}]
assert output["duplicate"]["body"] is None
assert output["duplicate"]["conflicts"] == [{"label": "Ship", "reason": "ambiguous"}]
+22
View File
@@ -121,6 +121,28 @@ process.stdout.write(JSON.stringify(drafts));
assert output[1]["label"] == "Queued issue"
def test_draft_inbox_identifies_account_bound_checklist_conflicts_for_review():
script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const record = {{version:2,items:[{{
id:'check-1',kind:'issue-content',ownerLogin:'timmy',status:'attention',repository:'o/r',number:7,
title:'Ship',baseBody:'- [ ] Build',body:'- [x] Build',expectedUpdatedAt:'old',error:'Issue changed',queuedAt:12,
}}]}};
const values = new Map([['stackchain.authored-outbox.v1', JSON.stringify(record)]]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const owned = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list()[0];
const other = createDraftInbox({{storage,getCurrentLogin:()=>'alexander'}}).list()[0];
process.stdout.write(JSON.stringify({{owned,other}}));
"""
output = run_node(script)
assert output["owned"]["label"] == "Checklist conflict"
assert output["owned"]["checklist_conflict"] is True
assert output["owned"]["quarantined"] is False
assert output["other"]["checklist_conflict"] is False
assert output["other"]["quarantined"] is True
def test_draft_inbox_exposes_queued_review_with_review_route_and_actionable_state():
script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
+19 -1
View File
@@ -33,6 +33,7 @@ LATER_PICKER = Path(__file__).parents[1] / "frontend" / "later-picker.js"
LATER_AND_START = Path(__file__).parents[1] / "frontend" / "later-and-start.js"
REVIEW_SHEET = Path(__file__).parents[1] / "frontend" / "review-sheet.js"
ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "issue-sheet.js"
FRONTEND_BUNDLE = Path(__file__).parents[1] / "src" / "frontend_bundle.py"
CREATE_ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "create-issue-sheet.js"
PULL_SHEET = Path(__file__).parents[1] / "frontend" / "pull-sheet.js"
CONVERSATION = Path(__file__).parents[1] / "frontend" / "conversation.js"
@@ -2224,6 +2225,22 @@ async def test_mobile_issue_detail_toggles_checklist_with_touch_safe_recovery():
assert '.checklist-pending .task-list-toggle { opacity:.65;' in html
@pytest.mark.anyio
async def test_mobile_drafts_reviews_and_retries_an_unambiguous_checklist_conflict():
html = await dashboard()
assert "mergeChecklistConflict: mergeChecklistConflict" in html
assert "const checklistConflict = item.checklist_conflict === true" in html
assert 'class="draft-review-checklist"' in html
assert ">Review changes</button>" in html
assert "const latest = await issueController.load({" in html
assert "authoredOutbox.resolveIssueContentConflict(item.outbox_id, latest, activeFlushLogin)" in html
assert "window.confirm('Apply ' + preview.changes.length + ' checklist change'" in html
assert "await authoredOutbox.retry(item.outbox_id, activeFlushLogin)" in html
assert "could not be matched safely" in html
assert "checklist-conflict.js" in FRONTEND_BUNDLE.read_text()
def test_completed_issue_checklist_offers_close_and_next_during_today():
script = f"""
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
@@ -2326,7 +2343,8 @@ pending.then(result => process.stdout.write(JSON.stringify({{before,queued,resul
assert output["before"] is False
assert output["queued"] == [{
"kind": "issue-content", "repository": "stackchain/api", "number": 17,
"title": "Release", "body": "- [x] Build\n- [X] Ship",
"title": "Release", "baseBody": "- [ ] Build\n- [X] Ship",
"body": "- [x] Build\n- [X] Ship",
"expectedUpdatedAt": "2026-08-15T10:00:00Z",
}]
assert output["result"] == {