feat: resolve mobile checklist conflicts (Closes #913)
This commit is contained in:
1 parent
7204a426af
commit
7b27f85b39
10 files changed
+281
-6
No files matched your search
@@ -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))});
|
||||
|
||||
@@ -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"}]
|
||||
@@ -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
@@ -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"] == {
|
||||
|
||||
Reference in new issue
Block a user