@@ -21,10 +21,11 @@ ok() { echo "✅ $1"; PASS=$((PASS+1)); }
2121# Build a configurable gh mock in a temp dir. It records every invocation to
2222# $CALLS and is driven by env vars set per scenario:
2323# MOCK_LABELS newline-separated labels returned by `pr view --json labels`
24- # MOCK_BASE base branch returned by `pr view --json baseRefName`
2524# MOCK_COMMENTS_FILE file whose contents are returned by `pr view --json comments`
2625# MOCK_LABELS_FAIL set to 1 to make `pr view --json labels` fail
2726# MOCK_PR_LIST_FAIL set to 1 to make `pr list` fail
27+ # The PR's base branch is not mocked: the script must take it from PR_BASE
28+ # (event payload), so a baseRefName query is an unhandled call and fails.
2829make_mock_gh () {
2930 local dir=" $1 "
3031 cat > " $dir /mock_gh.sh" << 'EOF '
@@ -36,8 +37,11 @@ if [[ "$1 $2" == "pr view" ]]; then
3637 *--json\ labels*)
3738 [[ "${MOCK_LABELS_FAIL:-}" == 1 ]] && { echo "mock gh: labels API down" >&2; exit 1; }
3839 printf '%s\n' "${MOCK_LABELS:-}";;
39- *--json\ baseRefName*) printf '%s\n' "${MOCK_BASE:-}";;
40- *--json\ comments*) cat "${MOCK_COMMENTS_FILE:-/dev/null}";;
40+ *--json\ comments*)
41+ # The comments file stands for our own comments only, so the query
42+ # must restrict itself to those.
43+ [[ "$*" == *viewerDidAuthor* ]] || { echo "comments query must filter by viewerDidAuthor" >&2; exit 1; }
44+ cat "${MOCK_COMMENTS_FILE:-/dev/null}";;
4145 *) echo "unhandled pr view: $*" >&2; exit 1;;
4246 esac
4347elif [[ "$1 $2" == "pr comment" ]]; then
@@ -93,11 +97,11 @@ setup_repo() {
9397}
9498
9599run_resume () {
96- env ACTION_MODE=conflict-resolved PR_BRANCH=child \
100+ env ACTION_MODE=conflict-resolved PR_BRANCH=child PR_NUMBER=5 PR_BASE= " $PR_BASE " \
97101 GH=" $MOCK_DIR /mock_gh.sh" GIT=" $MOCK_DIR /mock_git.sh" \
98- MOCK_LABELS=" $MOCK_LABELS " MOCK_BASE =" $MOCK_BASE " \
102+ MOCK_LABELS=" $MOCK_LABELS " MOCK_LABELS_FAIL =" ${MOCK_LABELS_FAIL :- } " \
99103 MOCK_COMMENTS_FILE=" $MOCK_COMMENTS_FILE " CALLS=" $CALLS " \
100- MOCK_LABELS_FAIL= " ${MOCK_LABELS_FAIL :- } " MOCK_PR_LIST_FAIL=" ${MOCK_PR_LIST_FAIL:- } " \
104+ MOCK_PR_LIST_FAIL=" ${MOCK_PR_LIST_FAIL:- } " \
101105 bash " $ROOT_DIR /update-pr-stack.sh" > " $WORK /out.log" 2>&1 || echo " EXIT=$? " >> " $WORK /out.log"
102106}
103107
@@ -109,7 +113,7 @@ marker() { # base target squash
109113echo " ### Scenario A: user manually retargeted the base -> no mutation"
110114setup_repo
111115MOCK_LABELS=" autorestack-needs-conflict-resolution"
112- MOCK_BASE =" spark" # human changed it; marker says parent
116+ PR_BASE =" spark" # human changed it; marker says parent
113117MOCK_COMMENTS_FILE=" $WORK /comments.txt"
114118{ echo " ### conflict" ; echo ; marker parent main " $SQUASH " ; } > " $MOCK_COMMENTS_FILE "
115119run_resume
@@ -125,7 +129,7 @@ ok "A: manual retarget detected, no branch mutation, label removed"
125129echo " ### Scenario B: no state marker -> no mutation"
126130setup_repo
127131MOCK_LABELS=" autorestack-needs-conflict-resolution"
128- MOCK_BASE =" parent"
132+ PR_BASE =" parent"
129133MOCK_COMMENTS_FILE=" $WORK /comments.txt"
130134{ echo " ### some old conflict comment with no marker" ; } > " $MOCK_COMMENTS_FILE "
131135run_resume
@@ -143,13 +147,13 @@ setup_repo
143147git -C " $WORK " merge -q --no-edit main
144148git -C " $WORK " push -q origin child
145149MOCK_LABELS=" autorestack-needs-conflict-resolution"
146- MOCK_BASE =" parent" # matches marker -> not a manual retarget
150+ PR_BASE =" parent" # matches marker -> not a manual retarget
147151MOCK_COMMENTS_FILE=" $WORK /comments.txt"
148152{ echo " ### conflict" ; echo ; marker parent main " $SQUASH " ; } > " $MOCK_COMMENTS_FILE "
149153run_resume
150154
151155grep -q -- " git push origin child" " $CALLS " || fail " C: child not pushed"
152- grep -q -- " pr edit child --base main" " $CALLS " || fail " C: base not retargeted to main"
156+ grep -q -- " pr edit 5 --base main" " $CALLS " || fail " C: base not retargeted to main"
153157grep -q " remove-label autorestack-needs-conflict-resolution" " $CALLS " || fail " C: label not removed"
154158push_line=$( grep -n -- " git push origin child" " $CALLS " | head -1 | cut -d: -f1)
155159base_line=$( grep -n -- " --base main" " $CALLS " | head -1 | cut -d: -f1)
@@ -162,7 +166,7 @@ ok "C: resume pushes, retargets base, then removes label"
162166echo " ### Scenario D: recorded target branch is gone -> give up cleanly"
163167setup_repo
164168MOCK_LABELS=" autorestack-needs-conflict-resolution"
165- MOCK_BASE =" parent" # matches marker -> not a manual retarget
169+ PR_BASE =" parent" # matches marker -> not a manual retarget
166170MOCK_COMMENTS_FILE=" $WORK /comments.txt"
167171{ echo " ### conflict" ; echo ; marker parent ghost-target " $SQUASH " ; } > " $MOCK_COMMENTS_FILE "
168172run_resume
@@ -174,7 +178,23 @@ grep -q -- "--base" "$CALLS" && fail "D: base must NOT be edited"
174178ok " D: missing target detected, no branch mutation, label removed"
175179
176180# ---------------------------------------------------------------------------
177- echo " ### Scenario E: recorded base branch is gone -> give up cleanly, no crash"
181+ echo " ### Scenario E: malformed state marker -> internal error, die without touching the PR"
182+ setup_repo
183+ MOCK_LABELS=" autorestack-needs-conflict-resolution"
184+ PR_BASE=" parent"
185+ MOCK_COMMENTS_FILE=" $WORK /comments.txt"
186+ { echo " ### conflict" ; echo ; echo ' <!-- autorestack-state: base=parent target=main -->' ; } > " $MOCK_COMMENTS_FILE "
187+ run_resume
188+
189+ grep -q " EXIT=1" " $WORK /out.log" || fail " E: run must die on a malformed marker"
190+ grep -q " remove-label" " $CALLS " && fail " E: label must stay on"
191+ grep -q " gh pr comment" " $CALLS " && fail " E: no PR comment for an internal error"
192+ grep -q -- " --base" " $CALLS " && fail " E: base must NOT be edited"
193+ [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " E: child was pushed"
194+ ok " E: malformed marker dies, PR untouched, label kept"
195+
196+ # ---------------------------------------------------------------------------
197+ echo " ### Scenario F: recorded base branch is gone -> give up cleanly, no crash"
178198setup_repo
179199# Advance main with the squash commit so the child is not already up to date and
180200# the resume would actually reach the merge step.
@@ -190,55 +210,55 @@ git checkout -q child
190210# repeating on every push.
191211git push -q origin " :parent"
192212MOCK_LABELS=" autorestack-needs-conflict-resolution"
193- MOCK_BASE =" parent" # matches marker -> not a manual retarget
213+ PR_BASE =" parent" # matches marker -> not a manual retarget
194214MOCK_COMMENTS_FILE=" $WORK /comments.txt"
195215{ echo " ### conflict" ; echo ; marker parent main " $SQUASH2 " ; } > " $MOCK_COMMENTS_FILE "
196216run_resume
197217
198- grep -q " EXIT=" " $WORK /out.log" && fail " E : script exited nonzero: $( cat " $WORK /out.log" ) "
199- grep -q " remove-label autorestack-needs-conflict-resolution" " $CALLS " || fail " E : label not removed"
200- grep -q -- " add-label" " $CALLS " && fail " E : conflict label must NOT be re-added"
201- grep -q " gh pr comment" " $CALLS " || fail " E : no explanatory comment posted"
202- grep -q -- " --base" " $CALLS " && fail " E : base must NOT be edited"
203- [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " E : child was pushed"
204- ok " E : missing base branch detected, no crash, label removed"
218+ grep -q " EXIT=" " $WORK /out.log" && fail " F : script exited nonzero: $( cat " $WORK /out.log" ) "
219+ grep -q " remove-label autorestack-needs-conflict-resolution" " $CALLS " || fail " F : label not removed"
220+ grep -q -- " add-label" " $CALLS " && fail " F : conflict label must NOT be re-added"
221+ grep -q " gh pr comment" " $CALLS " || fail " F : no explanatory comment posted"
222+ grep -q -- " --base" " $CALLS " && fail " F : base must NOT be edited"
223+ [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " F : child was pushed"
224+ ok " F : missing base branch detected, no crash, label removed"
205225
206226# ---------------------------------------------------------------------------
207- echo " ### Scenario F : reading the PR comments fails -> fail the run, keep the label"
227+ echo " ### Scenario G : reading the PR comments fails -> fail the run, keep the label"
208228setup_repo
209- # A transient API failure while reading the comments must not pass for "no
210- # state marker": that path removes the conflict label and permanently cancels
211- # the auto-resume. The run must fail loudly instead, so the next push retries.
229+ # An API failure must not pass for "no state marker": that path removes the
230+ # conflict label and permanently cancels the auto-resume. Fail loudly instead,
231+ # so the label stays on and the next push retries.
212232MOCK_LABELS=" autorestack-needs-conflict-resolution"
213- MOCK_BASE =" parent"
233+ PR_BASE =" parent"
214234MOCK_COMMENTS_FILE=" $WORK /does-not-exist" # makes the mock gh fail on pr view --json comments
215235run_resume
216236
217- grep -q " EXIT=" " $WORK /out.log" || fail " F : run should have failed"
218- grep -q " remove-label" " $CALLS " && fail " F : label must NOT be removed on an API failure"
219- grep -q " gh pr comment" " $CALLS " && fail " F : no comment must be posted on an API failure"
220- [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " F : child was pushed"
221- ok " F: API failure fails the run and keeps the resume armed"
237+ grep -q " EXIT=" " $WORK /out.log" || fail " G : run should have failed"
238+ grep -q " remove-label" " $CALLS " && fail " G : label must NOT be removed on an API failure"
239+ grep -q " gh pr comment" " $CALLS " && fail " G : no comment must be posted on an API failure"
240+ [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " G : child was pushed"
241+ ok " G: comments API failure fails the run and keeps the resume armed"
222242
223243# ---------------------------------------------------------------------------
224- echo " ### Scenario G : reading the labels fails -> fail the run, do nothing"
244+ echo " ### Scenario H : reading the labels fails -> fail the run, do nothing"
225245setup_repo
226246# An API failure must not pass for "no conflict label": that ends the run
227247# green without resuming anything.
228248MOCK_LABELS=" "
229249MOCK_LABELS_FAIL=1
230- MOCK_BASE =" parent"
250+ PR_BASE =" parent"
231251MOCK_COMMENTS_FILE=" $WORK /comments.txt"
232252{ echo " ### conflict" ; echo ; marker parent main " $SQUASH " ; } > " $MOCK_COMMENTS_FILE "
233253run_resume
234254
235- grep -q " EXIT=" " $WORK /out.log" || fail " G : run should have failed, not ended green"
236- grep -q " remove-label" " $CALLS " && fail " G : label must NOT be touched on an API failure"
237- [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " G : child was pushed"
238- ok " G : labels API failure fails the run instead of skipping the resume"
255+ grep -q " EXIT=" " $WORK /out.log" || fail " H : run should have failed, not ended green"
256+ grep -q " remove-label" " $CALLS " && fail " H : label must NOT be touched on an API failure"
257+ [[ " $( git -C " $ORIGIN " rev-parse child) " == " $CHILD_BEFORE " ]] || fail " H : child was pushed"
258+ ok " H : labels API failure fails the run instead of skipping the resume"
239259
240260# ---------------------------------------------------------------------------
241- echo " ### Scenario H : listing sibling conflicts fails -> keep the old base branch"
261+ echo " ### Scenario I : listing sibling conflicts fails -> keep the old base branch"
242262setup_repo
243263# Same successful-resume setup as scenario C, but the sibling listing that
244264# decides whether the old base branch can be deleted fails. Answering "no
@@ -247,16 +267,16 @@ git -C "$WORK" merge -q --no-edit main
247267git -C " $WORK " push -q origin child
248268MOCK_LABELS=" autorestack-needs-conflict-resolution"
249269MOCK_LABELS_FAIL=" "
250- MOCK_BASE =" parent"
270+ PR_BASE =" parent"
251271MOCK_PR_LIST_FAIL=1
252272MOCK_COMMENTS_FILE=" $WORK /comments.txt"
253273{ echo " ### conflict" ; echo ; marker parent main " $SQUASH " ; } > " $MOCK_COMMENTS_FILE "
254274run_resume
255275
256- grep -q " EXIT=" " $WORK /out.log" || fail " H : run should have failed"
257- git -C " $ORIGIN " rev-parse --verify -q parent > /dev/null || fail " H : old base branch was deleted on an API failure"
258- grep -q -- " push origin :parent" " $CALLS " && fail " H : deletion must not be attempted"
259- ok " H : sibling-listing API failure keeps the old base branch"
276+ grep -q " EXIT=" " $WORK /out.log" || fail " I : run should have failed"
277+ git -C " $ORIGIN " rev-parse --verify -q parent > /dev/null || fail " I : old base branch was deleted on an API failure"
278+ grep -q -- " push origin :parent" " $CALLS " && fail " I : deletion must not be attempted"
279+ ok " I : sibling-listing API failure keeps the old base branch"
260280
261281echo
262282echo " All conflict-resume tests passed 🎉 ($PASS scenarios)"
0 commit comments