Skip to content

Commit dd31c3c

Browse files
Phlogistiqueclaudegithub-actions
authored
Add e2e tests for fan-out edge cases and fix test infra bugs (#29)
Three new e2e scenarios covering previously untested fan-out cases: - **Scenario 4**: 2 children, both merge cleanly - **Scenario 5**: 2 children, mixed outcome (one conflicts, one succeeds) - **Scenario 6**: 0 children Also fixes two bugs in the test infrastructure that were discovered while running the new tests: **UTC timezone mismatch in `wait_for_synchronize_workflow`** — The `createdAt` filter used `date -Iseconds` which produces local time with offset (e.g. `17:25:03+01:00`), but GitHub returns UTC (`16:25:07Z`). Since jq does string comparison, the local-time string was always lexicographically greater, filtering out all matching runs. This caused flaky timeouts on Scenario 2 (conflict resolution continuation). **`compare_diffs` now strips blob SHA index lines** — When a PR's base branch changes (e.g. from `feature11` to `main`), the `index` line blob SHAs legitimately differ even though the actual text diff is identical. **Base file expanded to 14 lines** — The original 7-line file meant adjacent lines (5 and 6) fell within git's 3-line context window, causing false merge conflicts in the mixed-outcome scenario. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions <github-actions@github.com>
1 parent e306824 commit dd31c3c

1 file changed

Lines changed: 329 additions & 3 deletions

File tree

tests/test_e2e.sh

Lines changed: 329 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@
9797
# - Deletes feature2 branch (no other conflicted PRs depend on it)
9898
# - NOTE: feature4 is NOT updated (indirect children are not modified)
9999
#
100+
# SCENARIO 4: Multi-child with 0 conflicts (Steps 23-25)
101+
# -------------------------------------------------------
102+
# Tests that when a PR with 2 children is merged and neither conflicts,
103+
# both children are cleanly updated, diffs preserved, and old base deleted.
104+
#
105+
# SCENARIO 5: Multi-child with mixed outcome (Steps 26-28)
106+
# ---------------------------------------------------------
107+
# Tests that when one child conflicts and the other merges cleanly, the
108+
# clean child is fully updated while the conflicted child keeps the old base.
109+
# The old base branch is kept for the conflicted child.
110+
#
111+
# SCENARIO 6: No direct children / 0-child run (Steps 29-31)
112+
# -----------------------------------------------------------
113+
# Tests that merging a PR with no children simply deletes the branch
114+
# and the action completes successfully.
115+
#
100116
# =============================================================================
101117
set -e # Exit immediately if a command exits with a non-zero status.
102118
# set -x # Debugging: print commands as they are executed
@@ -165,13 +181,18 @@ get_pr_diff() {
165181
}
166182

167183
# Compare two diffs and return 0 if identical, 1 if different.
168-
# Also prints a message describing the result.
184+
# Strips "index" lines (blob SHA pairs) since those change legitimately
185+
# when the base branch changes.
169186
compare_diffs() {
170187
local diff1="$1"
171188
local diff2="$2"
172189
local context="$3"
173190

174-
if [[ "$diff1" == "$diff2" ]]; then
191+
local stripped1 stripped2
192+
stripped1=$(echo "$diff1" | grep -v '^index ')
193+
stripped2=$(echo "$diff2" | grep -v '^index ')
194+
195+
if [[ "$stripped1" == "$stripped2" ]]; then
175196
echo >&2 "✅ Diffs match: $context"
176197
return 0
177198
else
@@ -265,7 +286,7 @@ wait_for_synchronize_workflow() {
265286
--workflow "$WORKFLOW_FILE" \
266287
--event pull_request \
267288
--limit 15 \
268-
--json databaseId,createdAt --jq '.[] | select(.createdAt >= "'$(date -d "@$start_time" -Iseconds 2>/dev/null || date -r $start_time +%Y-%m-%dT%H:%M:%S)'") | .databaseId' || echo "")
289+
--json databaseId,createdAt --jq '.[] | select(.createdAt >= "'$(date -u -d "@$start_time" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -r $start_time +%Y-%m-%dT%H:%M:%SZ)'") | .databaseId' || echo "")
269290

270291
if [[ -z "$candidate_run_ids" ]]; then
271292
echo >&2 "No recent '$WORKFLOW_FILE' runs found since start. Sleeping $sleep_time seconds."
@@ -483,6 +504,13 @@ echo "Base file content line 4" >> file.txt
483504
echo "Base file content line 5" >> file.txt
484505
echo "Base file content line 6" >> file.txt
485506
echo "Base file content line 7" >> file.txt
507+
echo "Base file content line 8" >> file.txt
508+
echo "Base file content line 9" >> file.txt
509+
echo "Base file content line 10" >> file.txt
510+
echo "Base file content line 11" >> file.txt
511+
echo "Base file content line 12" >> file.txt
512+
echo "Base file content line 13" >> file.txt
513+
echo "Base file content line 14" >> file.txt
486514
log_cmd git add file.txt
487515
log_cmd git commit -m "Initial commit"
488516
INITIAL_COMMIT_SHA=$(git rev-parse HEAD)
@@ -1194,6 +1222,304 @@ fi
11941222
echo >&2 "--- Sibling Conflicts Scenario Test Completed Successfully ---"
11951223

11961224

1225+
# --- SCENARIO 4: Multi-child with 0 conflicts ---
1226+
# ===================================================================================
1227+
# Tests that when a PR with multiple children is merged and none conflict,
1228+
# all children are cleanly updated and the old base branch is deleted.
1229+
#
1230+
# Setup:
1231+
# - Create main <- feature8 <- (feature9, feature10) parallel children
1232+
# - feature9 and feature10 modify different lines (no conflict with each other or main)
1233+
#
1234+
# Expected Behavior:
1235+
# - After merging feature8, both feature9 and feature10 are cleanly rebased
1236+
# - Both PRs' base branches updated to main
1237+
# - feature8 branch is deleted (no conflicts)
1238+
# ===================================================================================
1239+
1240+
echo >&2 "--- Testing Multi-child No Conflicts Scenario ---"
1241+
1242+
echo >&2 "23. Creating stack for multi-child no-conflict test..."
1243+
log_cmd git checkout main
1244+
log_cmd git pull origin main
1245+
1246+
# Create feature8 based on main
1247+
log_cmd git checkout -b feature8 main
1248+
sed -i '2s/.*/Feature 8 content line 2/' file.txt
1249+
log_cmd git add file.txt
1250+
log_cmd git commit -m "Add feature 8"
1251+
log_cmd git push origin feature8
1252+
PR8_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base main --head feature8 --title "Feature 8" --body "This is PR 8")
1253+
PR8_NUM=$(echo "$PR8_URL" | awk -F'/' '{print $NF}')
1254+
echo >&2 "Created PR #$PR8_NUM: $PR8_URL"
1255+
1256+
# Create feature9 based on feature8 (modifies line 3 — no conflict)
1257+
log_cmd git checkout -b feature9 feature8
1258+
sed -i '3s/.*/Feature 9 content line 3/' file.txt
1259+
log_cmd git add file.txt
1260+
log_cmd git commit -m "Add feature 9 (modifies line 3)"
1261+
log_cmd git push origin feature9
1262+
PR9_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base feature8 --head feature9 --title "Feature 9" --body "This is PR 9, child of PR 8")
1263+
PR9_NUM=$(echo "$PR9_URL" | awk -F'/' '{print $NF}')
1264+
echo >&2 "Created PR #$PR9_NUM: $PR9_URL"
1265+
1266+
# Capture PR9 diff before merge
1267+
PR9_DIFF_BEFORE=$(get_pr_diff "$PR9_URL")
1268+
1269+
# Create feature10 based on feature8 (modifies line 4 — no conflict)
1270+
log_cmd git checkout feature8
1271+
log_cmd git checkout -b feature10
1272+
sed -i '4s/.*/Feature 10 content line 4/' file.txt
1273+
log_cmd git add file.txt
1274+
log_cmd git commit -m "Add feature 10 (modifies line 4)"
1275+
log_cmd git push origin feature10
1276+
PR10_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base feature8 --head feature10 --title "Feature 10" --body "This is PR 10, child of PR 8")
1277+
PR10_NUM=$(echo "$PR10_URL" | awk -F'/' '{print $NF}')
1278+
echo >&2 "Created PR #$PR10_NUM: $PR10_URL"
1279+
1280+
# Capture PR10 diff before merge
1281+
PR10_DIFF_BEFORE=$(get_pr_diff "$PR10_URL")
1282+
1283+
# 24. Merge feature8 to trigger clean updates on both children
1284+
echo >&2 "24. Squash merging PR #$PR8_NUM (feature8)..."
1285+
merge_pr_with_retry "$PR8_URL"
1286+
MERGE_COMMIT_SHA8=$(gh pr view "$PR8_URL" --repo "$REPO_FULL_NAME" --json mergeCommit -q .mergeCommit.oid)
1287+
echo >&2 "PR #$PR8_NUM merged. Squash commit SHA: $MERGE_COMMIT_SHA8"
1288+
1289+
echo >&2 "Waiting for workflow..."
1290+
if ! wait_for_workflow "$PR8_NUM" "feature8" "$MERGE_COMMIT_SHA8" "success"; then
1291+
echo >&2 "Workflow for PR8 merge did not complete successfully."
1292+
exit 1
1293+
fi
1294+
1295+
# 25. Verify both children updated cleanly
1296+
echo >&2 "25. Verifying multi-child clean update..."
1297+
log_cmd git fetch origin --prune
1298+
1299+
# feature8 branch should be deleted (no conflicts)
1300+
if git show-ref --verify --quiet refs/remotes/origin/feature8; then
1301+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature8' still exists."
1302+
exit 1
1303+
else
1304+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature8' was deleted."
1305+
fi
1306+
1307+
# Both PRs should have main as base
1308+
PR9_BASE=$(gh pr view "$PR9_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1309+
PR10_BASE=$(gh pr view "$PR10_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1310+
1311+
if [[ "$PR9_BASE" == "main" && "$PR10_BASE" == "main" ]]; then
1312+
echo >&2 "✅ Verification Passed: Both PRs updated to base 'main'."
1313+
else
1314+
echo >&2 "❌ Verification Failed: PR9 base='$PR9_BASE', PR10 base='$PR10_BASE', expected both 'main'."
1315+
exit 1
1316+
fi
1317+
1318+
# Neither PR should have conflict labels
1319+
PR9_LABEL=$(gh pr view "$PR9_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
1320+
PR10_LABEL=$(gh pr view "$PR10_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
1321+
1322+
if [[ -z "$PR9_LABEL" && -z "$PR10_LABEL" ]]; then
1323+
echo >&2 "✅ Verification Passed: Neither PR has conflict labels."
1324+
else
1325+
echo >&2 "❌ Verification Failed: PR9 label='$PR9_LABEL', PR10 label='$PR10_LABEL'."
1326+
exit 1
1327+
fi
1328+
1329+
# Diffs should be preserved
1330+
PR9_DIFF_AFTER=$(get_pr_diff "$PR9_URL")
1331+
PR10_DIFF_AFTER=$(get_pr_diff "$PR10_URL")
1332+
compare_diffs "$PR9_DIFF_BEFORE" "$PR9_DIFF_AFTER" "PR9 diff preserved after multi-child clean update"
1333+
compare_diffs "$PR10_DIFF_BEFORE" "$PR10_DIFF_AFTER" "PR10 diff preserved after multi-child clean update"
1334+
1335+
echo >&2 "--- Multi-child No Conflicts Scenario Test Completed Successfully ---"
1336+
1337+
1338+
# --- SCENARIO 5: Multi-child with mixed outcome (one conflicts, one succeeds) ---
1339+
# ===================================================================================
1340+
# Tests that when one child conflicts and the other merges cleanly, the old base
1341+
# branch is kept (for the conflicted child) and the clean child is fully updated.
1342+
#
1343+
# Setup:
1344+
# - Create main <- feature11 <- (feature12, feature13) parallel children
1345+
# - feature12 modifies line 5 (will conflict with a main change)
1346+
# - feature13 modifies line 14 (no conflict — far enough from line 5 to avoid overlapping hunks)
1347+
# - Push a conflicting change to line 5 on main
1348+
#
1349+
# Expected Behavior:
1350+
# - feature13 is cleanly updated, base changed to main
1351+
# - feature12 gets conflict label, base stays feature11
1352+
# - feature11 branch is kept (still referenced by conflicted PR12)
1353+
# ===================================================================================
1354+
1355+
echo >&2 "--- Testing Multi-child Mixed Outcome Scenario ---"
1356+
1357+
echo >&2 "26. Creating stack for mixed outcome test..."
1358+
log_cmd git checkout main
1359+
log_cmd git pull origin main
1360+
1361+
# Create feature11 based on main
1362+
log_cmd git checkout -b feature11 main
1363+
sed -i '2s/.*/Feature 11 content line 2/' file.txt
1364+
log_cmd git add file.txt
1365+
log_cmd git commit -m "Add feature 11"
1366+
log_cmd git push origin feature11
1367+
PR11_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base main --head feature11 --title "Feature 11" --body "This is PR 11")
1368+
PR11_NUM=$(echo "$PR11_URL" | awk -F'/' '{print $NF}')
1369+
echo >&2 "Created PR #$PR11_NUM: $PR11_URL"
1370+
1371+
# Create feature12 based on feature11 (modifies line 5 — will conflict)
1372+
log_cmd git checkout -b feature12 feature11
1373+
sed -i '5s/.*/Feature 12 conflicting content line 5/' file.txt
1374+
log_cmd git add file.txt
1375+
log_cmd git commit -m "Add feature 12 (modifies line 5)"
1376+
log_cmd git push origin feature12
1377+
PR12_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base feature11 --head feature12 --title "Feature 12" --body "This is PR 12, child of PR 11")
1378+
PR12_NUM=$(echo "$PR12_URL" | awk -F'/' '{print $NF}')
1379+
echo >&2 "Created PR #$PR12_NUM: $PR12_URL"
1380+
1381+
# Create feature13 based on feature11 (modifies line 6 — no conflict)
1382+
log_cmd git checkout feature11
1383+
log_cmd git checkout -b feature13
1384+
sed -i '14s/.*/Feature 13 content line 14/' file.txt
1385+
log_cmd git add file.txt
1386+
log_cmd git commit -m "Add feature 13 (modifies line 14)"
1387+
log_cmd git push origin feature13
1388+
PR13_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base feature11 --head feature13 --title "Feature 13" --body "This is PR 13, child of PR 11")
1389+
PR13_NUM=$(echo "$PR13_URL" | awk -F'/' '{print $NF}')
1390+
echo >&2 "Created PR #$PR13_NUM: $PR13_URL"
1391+
1392+
# Capture PR13 diff before merge
1393+
PR13_DIFF_BEFORE=$(get_pr_diff "$PR13_URL")
1394+
1395+
# Push conflicting change to main (line 5)
1396+
log_cmd git checkout main
1397+
sed -i '5s/.*/Main conflicting content line 5 for scenario 5/' file.txt
1398+
log_cmd git add file.txt
1399+
log_cmd git commit -m "Add conflicting change on main line 5 (scenario 5)"
1400+
log_cmd git push origin main
1401+
1402+
# 27. Merge feature11 to trigger mixed outcome
1403+
echo >&2 "27. Squash merging PR #$PR11_NUM (feature11)..."
1404+
merge_pr_with_retry "$PR11_URL"
1405+
MERGE_COMMIT_SHA11=$(gh pr view "$PR11_URL" --repo "$REPO_FULL_NAME" --json mergeCommit -q .mergeCommit.oid)
1406+
echo >&2 "PR #$PR11_NUM merged. Squash commit SHA: $MERGE_COMMIT_SHA11"
1407+
1408+
echo >&2 "Waiting for workflow..."
1409+
if ! wait_for_workflow "$PR11_NUM" "feature11" "$MERGE_COMMIT_SHA11" "success"; then
1410+
echo >&2 "Workflow for PR11 merge did not complete successfully."
1411+
exit 1
1412+
fi
1413+
1414+
# 28. Verify mixed outcome
1415+
echo >&2 "28. Verifying mixed outcome..."
1416+
log_cmd git fetch origin
1417+
1418+
# feature11 branch should still exist (feature12 is conflicted)
1419+
if git show-ref --verify --quiet refs/remotes/origin/feature11; then
1420+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature11' still exists (kept for conflicted PR12)."
1421+
else
1422+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature11' was deleted prematurely."
1423+
exit 1
1424+
fi
1425+
1426+
# PR13 (clean child) should have main as base
1427+
PR13_BASE=$(gh pr view "$PR13_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1428+
if [[ "$PR13_BASE" == "main" ]]; then
1429+
echo >&2 "✅ Verification Passed: PR #$PR13_NUM (clean child) base updated to 'main'."
1430+
else
1431+
echo >&2 "❌ Verification Failed: PR #$PR13_NUM base is '$PR13_BASE', expected 'main'."
1432+
exit 1
1433+
fi
1434+
1435+
# PR13 should not have conflict label
1436+
PR13_LABEL=$(gh pr view "$PR13_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
1437+
if [[ -z "$PR13_LABEL" ]]; then
1438+
echo >&2 "✅ Verification Passed: PR #$PR13_NUM has no conflict label."
1439+
else
1440+
echo >&2 "❌ Verification Failed: PR #$PR13_NUM has conflict label."
1441+
exit 1
1442+
fi
1443+
1444+
# PR13 diff should be preserved (compare_diffs strips blob SHAs which change with the base)
1445+
PR13_DIFF_AFTER=$(get_pr_diff "$PR13_URL")
1446+
compare_diffs "$PR13_DIFF_BEFORE" "$PR13_DIFF_AFTER" "PR13 diff preserved after mixed outcome"
1447+
1448+
# PR12 (conflicting child) should still have feature11 as base
1449+
PR12_BASE=$(gh pr view "$PR12_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1450+
if [[ "$PR12_BASE" == "feature11" ]]; then
1451+
echo >&2 "✅ Verification Passed: PR #$PR12_NUM (conflicted child) base stays 'feature11'."
1452+
else
1453+
echo >&2 "❌ Verification Failed: PR #$PR12_NUM base is '$PR12_BASE', expected 'feature11'."
1454+
exit 1
1455+
fi
1456+
1457+
# PR12 should have conflict label
1458+
PR12_LABEL=$(gh pr view "$PR12_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
1459+
if [[ "$PR12_LABEL" == "autorestack-needs-conflict-resolution" ]]; then
1460+
echo >&2 "✅ Verification Passed: PR #$PR12_NUM has conflict label."
1461+
else
1462+
echo >&2 "❌ Verification Failed: PR #$PR12_NUM does not have conflict label."
1463+
exit 1
1464+
fi
1465+
1466+
echo >&2 "--- Multi-child Mixed Outcome Scenario Test Completed Successfully ---"
1467+
1468+
1469+
# --- SCENARIO 6: No direct children (0-child run) ---
1470+
# ===================================================================================
1471+
# Tests that merging a PR with no children completes successfully and simply
1472+
# deletes the merged branch.
1473+
#
1474+
# Setup:
1475+
# - Create main <- feature14 with no children
1476+
#
1477+
# Expected Behavior:
1478+
# - Action runs and completes with success
1479+
# - feature14 branch is deleted
1480+
# ===================================================================================
1481+
1482+
echo >&2 "--- Testing No Children Scenario ---"
1483+
1484+
echo >&2 "29. Creating standalone PR with no children..."
1485+
log_cmd git checkout main
1486+
log_cmd git pull origin main
1487+
1488+
log_cmd git checkout -b feature14 main
1489+
sed -i '2s/.*/Feature 14 content line 2/' file.txt
1490+
log_cmd git add file.txt
1491+
log_cmd git commit -m "Add feature 14"
1492+
log_cmd git push origin feature14
1493+
PR14_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base main --head feature14 --title "Feature 14" --body "This is PR 14, no children")
1494+
PR14_NUM=$(echo "$PR14_URL" | awk -F'/' '{print $NF}')
1495+
echo >&2 "Created PR #$PR14_NUM: $PR14_URL"
1496+
1497+
# 30. Merge feature14
1498+
echo >&2 "30. Squash merging PR #$PR14_NUM (feature14, no children)..."
1499+
merge_pr_with_retry "$PR14_URL"
1500+
MERGE_COMMIT_SHA14=$(gh pr view "$PR14_URL" --repo "$REPO_FULL_NAME" --json mergeCommit -q .mergeCommit.oid)
1501+
echo >&2 "PR #$PR14_NUM merged. Squash commit SHA: $MERGE_COMMIT_SHA14"
1502+
1503+
echo >&2 "Waiting for workflow..."
1504+
if ! wait_for_workflow "$PR14_NUM" "feature14" "$MERGE_COMMIT_SHA14" "success"; then
1505+
echo >&2 "Workflow for PR14 merge did not complete successfully."
1506+
exit 1
1507+
fi
1508+
1509+
# 31. Verify branch was deleted and nothing broke
1510+
echo >&2 "31. Verifying no-children outcome..."
1511+
log_cmd git fetch origin --prune
1512+
1513+
if git show-ref --verify --quiet refs/remotes/origin/feature14; then
1514+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature14' still exists."
1515+
exit 1
1516+
else
1517+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature14' was deleted."
1518+
fi
1519+
1520+
echo >&2 "--- No Children Scenario Test Completed Successfully ---"
1521+
1522+
11971523
# --- Test Succeeded ---
11981524
echo >&2 "--- E2E Test Completed Successfully! ---"
11991525

0 commit comments

Comments
 (0)