Skip to content

Claim exclusive ownership for concurrent rebase continue/abort - #2004

Merged
timsehn merged 8 commits into
masterfrom
fix/rebase-concurrent-continue-abort
Aug 7, 2026
Merged

Claim exclusive ownership for concurrent rebase continue/abort#2004
timsehn merged 8 commits into
masterfrom
fix/rebase-concurrent-continue-abort

Conversation

@timsehn

@timsehn timsehn commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes concurrent dolt_rebase('--continue') vs dolt_rebase('--abort') both failing.

Before: both connections saw session isRebasing, both ran multi-step cleanup, both could report rebase failed / rebase recovery failed.

After: one side claims the end of the rebase under the graph lock (reload durable working-set state → clear isRebasing + persist → drop dolt_rebase). The loser gets no rebase in progress. Cleanup after claim is idempotent if the temp working branch is already gone.

Matches Dolt’s abortRebase / validateActiveRebase spirit: once working-set rebase state is cleared, the other session should see no active rebase.

Test plan

  • multi_process_merge_rebase_test — 44/44 (includes new Test 5: 12 concurrent continue/abort trials)
  • test/doltlite_rebase.sh — 24/24
  • pthread sequential abort→continue / continue→abort: loser always no rebase in progress

Tim added 2 commits August 5, 2026 19:20
When --continue and --abort race, both could pass the in-session
isRebasing check and then both fail mid-cleanup ("rebase failed" /
"rebase recovery failed"), leaving recovery unpredictable.

Add rebaseClaimActiveEnd: under the graph lock, reload durable working
set state; if isRebasing is already clear return SQLITE_DONE ("no rebase
in progress"); otherwise clear the flag, persist it, and drop
dolt_rebase so only one side proceeds. Abort and continue both claim
before cleanup/replay. Make post-claim cleanup idempotent when the
temporary working branch is already gone, so a lost race reports
"no rebase in progress" instead of a stuck recovery failure.

Absent plan table also reports "no rebase in progress" for consistency.

Test: multi_process_merge_rebase_test races continue vs abort across
forked connections for 12 trials.
Checked builds use -Werror=unused-result; ignore-free write() on the
child result pipe failed CI on Ubuntu.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

DoltLite source coverage

Metric Covered Total Coverage
Lines 43295 51320 84.36%
Branches 18999 28414 66.86%
Functions 2113 2314 91.31%

Merged 165 pooled raw profiles from the distributed Linux correctness jobs.

Per-file coverage (91 files)
File Lines Branches Functions
src/btree_orig_api.c 86.32% 76.19% 87.50%
src/chunk_file.c 100.00% 100.00% 100.00%
src/chunk_index.c 88.76% 76.47% 100.00%
src/chunk_refs.c 99.35% 85.48% 100.00%
src/chunk_staging.c 87.50% 77.05% 94.12%
src/chunk_store.c 91.01% 75.00% 100.00%
src/chunk_store_commit.c 86.32% 66.96% 100.00%
src/chunk_store_lock.c 94.25% 77.47% 100.00%
src/chunk_store_refs_api.c 91.52% 77.40% 100.00%
src/chunk_wal.c 94.24% 75.00% 92.86%
src/doltlite.c 100.00% 100.00% 100.00%
src/doltlite_add.c 85.19% 67.71% 100.00%
src/doltlite_ancestor.c 88.68% 66.11% 100.00%
src/doltlite_at.c 77.98% 59.93% 94.12%
src/doltlite_blame.c 80.70% 58.94% 96.43%
src/doltlite_branch.c 86.78% 80.20% 100.00%
src/doltlite_branches.c 93.51% 77.27% 92.86%
src/doltlite_checkout.c 76.38% 59.81% 100.00%
src/doltlite_cherry_pick.c 70.53% 55.08% 71.43%
src/doltlite_chunk_walk.c 88.60% 64.19% 100.00%
src/doltlite_cmd.c 69.23% 60.83% 93.33%
src/doltlite_commit.c 93.22% 73.08% 100.00%
src/doltlite_commit_ancestors.c 90.65% 68.89% 92.31%
src/doltlite_commit_cmd.c 75.72% 69.42% 100.00%
src/doltlite_config.c 77.54% 71.43% 100.00%
src/doltlite_conflicts.c 81.66% 58.83% 92.06%
src/doltlite_constraint_violations.c 81.26% 55.88% 90.91%
src/doltlite_core.c 91.69% 72.22% 100.00%
src/doltlite_creds.c 84.01% 54.75% 89.74%
src/doltlite_dbpage.c 92.68% 73.21% 91.67%
src/doltlite_diff.c 87.88% 69.04% 96.43%
src/doltlite_diff_stat.c 94.15% 75.38% 95.56%
src/doltlite_diff_table.c 93.78% 68.45% 97.14%
src/doltlite_gc.c 77.11% 56.01% 96.30%
src/doltlite_hashof.c 75.64% 62.86% 94.44%
src/doltlite_history.c 85.61% 70.55% 100.00%
src/doltlite_http_remote.c 82.22% 57.17% 92.31%
src/doltlite_ignore.c 90.06% 66.67% 100.00%
src/doltlite_log.c 94.38% 70.16% 92.31%
src/doltlite_merge.c 96.94% 74.49% 100.00%
src/doltlite_merge_cmd.c 88.89% 73.50% 100.00%
src/doltlite_merge_constraints.c 88.25% 67.65% 100.00%
src/doltlite_merge_constraints_check.c 92.68% 65.76% 100.00%
src/doltlite_merge_constraints_fk.c 77.09% 55.86% 85.71%
src/doltlite_merge_constraints_unique.c 86.50% 64.37% 100.00%
src/doltlite_merge_pass1.c 91.02% 74.87% 100.00%
src/doltlite_merge_pass2.c 62.90% 57.69% 100.00%
src/doltlite_merge_rows.c 88.25% 69.37% 100.00%
src/doltlite_merge_schema.c 84.75% 63.03% 96.55%
src/doltlite_merge_status.c 91.98% 73.53% 92.31%
src/doltlite_patch.c 94.92% 74.25% 97.83%
src/doltlite_rebase.c 84.48% 57.63% 97.30%
src/doltlite_record.c 75.90% 57.54% 92.86%
src/doltlite_ref.c 96.97% 77.27% 100.00%
src/doltlite_remote.c 81.98% 63.65% 95.92%
src/doltlite_remote_sql.c 63.89% 55.98% 92.86%
src/doltlite_remotesrv.c 75.94% 64.12% 90.62%
src/doltlite_reset.c 86.10% 71.53% 100.00%
src/doltlite_revert.c 79.29% 68.18% 100.00%
src/doltlite_schema_diff.c 92.38% 69.46% 96.88%
src/doltlite_schemas.c 68.97% 44.87% 90.91%
src/doltlite_status.c 92.46% 71.84% 97.22%
src/doltlite_tag.c 80.07% 56.62% 93.33%
src/doltlite_tls.c 85.04% 62.73% 92.31%
src/doltlite_verify_constraints.c 74.11% 58.46% 100.00%
src/doltlite_workspace.c 89.46% 65.50% 100.00%
src/pager_shim.c 58.00% 64.73% 32.37%
src/prolly_btree.c 81.21% 63.06% 86.40%
src/prolly_btree_catalog.c 80.81% 66.17% 96.67%
src/prolly_btree_cursor.c 86.32% 62.69% 97.92%
src/prolly_btree_cursor_count.c 78.78% 55.70% 100.00%
src/prolly_btree_cursor_payload.c 78.81% 60.38% 94.12%
src/prolly_btree_cursor_seek.c 73.88% 61.69% 82.35%
src/prolly_btree_mutation.c 85.54% 65.05% 97.92%
src/prolly_btree_orig.c 84.69% 35.71% 89.04%
src/prolly_btree_state.c 93.89% 67.48% 100.00%
src/prolly_btree_txn.c 82.27% 69.21% 98.04%
src/prolly_cache.c 93.24% 69.70% 100.00%
src/prolly_check.c 60.36% 62.96% 100.00%
src/prolly_chunker.c 94.04% 78.38% 100.00%
src/prolly_cursor.c 89.65% 81.25% 100.00%
src/prolly_diff.c 52.48% 38.62% 60.87%
src/prolly_hash.c 93.65% 80.00% 100.00%
src/prolly_hashset.c 90.48% 80.56% 100.00%
src/prolly_mutate.c 87.07% 78.08% 100.00%
src/prolly_mutmap.c 92.95% 80.40% 100.00%
src/prolly_node.c 89.31% 74.24% 100.00%
src/prolly_three_way_diff.c 95.58% 85.90% 100.00%
src/prolly_three_way_merge.c 80.78% 66.67% 91.67%
src/prolly_xxhash.c 100.00% 100.00% 100.00%
src/sortkey.c 93.06% 80.14% 100.00%

Download HTML and LCOV artifacts from this workflow run.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

DoltLite performance vs PR base

  • Baseline: b92cd62873d350b7da2fb28e06a31ecea744618c
  • Candidate: 452826c62593775117b85d2795d7ff287f11918f
  • Overall ratio: 1.003x
  • Gate result: PASS
  • Gates: individual > 1.25x with more than 5.00ms regression; section, suite, or overall > 1.15x with the same minimum delta
  • vc individual gate: > 1.50x with more than 25.00ms regression
  • Confirmed failed gates: none
  • Automatic retries: none
Suite Workloads Baseline total Candidate total Ratio Result
blobpk 69 11.92s 11.98s 1.005x PASS
compositepk 69 12.12s 12.20s 1.007x PASS
int 69 11.05s 11.03s 0.998x PASS
textpk 69 11.61s 11.63s 1.001x PASS
vc 13 977.59ms 980.45ms 1.003x PASS
blobpk details
Section Test Baseline Candidate Delta Ratio Result
mem_reads oltp_point_select 36.89ms 36.56ms -329us 0.991x PASS
mem_reads oltp_range_select 14.13ms 14.23ms +108us 1.008x PASS
mem_reads oltp_sum_range 13.84ms 13.89ms +51us 1.004x PASS
mem_reads oltp_order_range 3.21ms 3.17ms -37us 0.988x PASS
mem_reads oltp_distinct_range 4.25ms 4.28ms +28us 1.007x PASS
mem_reads oltp_index_scan 6.40ms 6.41ms +7us 1.001x PASS
mem_reads select_random_points 20.50ms 20.86ms +363us 1.018x PASS
mem_reads select_random_ranges 5.15ms 5.20ms +42us 1.008x PASS
mem_reads covering_index_scan 4.43ms 4.47ms +43us 1.010x PASS
mem_reads groupby_scan 34.12ms 34.03ms -91us 0.997x PASS
mem_reads index_join 9.15ms 9.11ms -39us 0.996x PASS
mem_reads index_join_scan 5.38ms 5.38ms -6us 0.999x PASS
mem_reads types_table_scan 1.24s 1.29s +44.53ms 1.036x PASS
mem_reads table_scan 1.39s 1.38s -9.21ms 0.993x PASS
mem_reads oltp_read_only 137.73ms 138.10ms +370us 1.003x PASS
mem_writes oltp_bulk_insert 356.41ms 357.10ms +686us 1.002x PASS
mem_writes oltp_insert 39.44ms 39.40ms -39us 0.999x PASS
mem_writes oltp_update_index 129.35ms 129.63ms +287us 1.002x PASS
mem_writes oltp_update_non_index 83.54ms 84.21ms +674us 1.008x PASS
mem_writes oltp_delete_insert 101.66ms 102.99ms +1.33ms 1.013x PASS
mem_writes oltp_write_only 61.67ms 61.00ms -671us 0.989x PASS
mem_writes types_delete_insert 54.48ms 53.75ms -730us 0.987x PASS
mem_writes oltp_read_write 140.68ms 139.03ms -1.64ms 0.988x PASS
file_reads oltp_point_select 61.31ms 61.45ms +147us 1.002x PASS
file_reads oltp_range_select 16.70ms 17.05ms +345us 1.021x PASS
file_reads oltp_sum_range 16.73ms 16.89ms +158us 1.009x PASS
file_reads oltp_order_range 3.59ms 3.59ms -6us 0.998x PASS
file_reads oltp_distinct_range 4.64ms 4.72ms +82us 1.018x PASS
file_reads oltp_index_scan 8.95ms 9.00ms +57us 1.006x PASS
file_reads select_random_points 24.07ms 24.10ms +29us 1.001x PASS
file_reads select_random_ranges 7.69ms 7.66ms -30us 0.996x PASS
file_reads covering_index_scan 7.15ms 7.15ms -3us 1.000x PASS
file_reads groupby_scan 34.82ms 34.95ms +126us 1.004x PASS
file_reads index_join 11.25ms 11.12ms -134us 0.988x PASS
file_reads index_join_scan 5.89ms 5.80ms -83us 0.986x PASS
file_reads types_table_scan 1.24s 1.27s +31.77ms 1.026x PASS
file_reads table_scan 1.38s 1.37s -5.36ms 0.996x PASS
file_reads oltp_read_only 171.81ms 173.88ms +2.07ms 1.012x PASS
file_writes oltp_bulk_insert 381.51ms 379.57ms -1.94ms 0.995x PASS
file_writes oltp_insert 51.61ms 51.58ms -32us 0.999x PASS
file_writes oltp_update_index 161.62ms 164.17ms +2.55ms 1.016x PASS
file_writes oltp_update_non_index 111.10ms 108.31ms -2.79ms 0.975x PASS
file_writes oltp_delete_insert 130.05ms 130.26ms +202us 1.002x PASS
file_writes oltp_write_only 83.96ms 83.53ms -429us 0.995x PASS
file_writes types_delete_insert 71.37ms 72.65ms +1.28ms 1.018x PASS
file_writes oltp_read_write 162.27ms 162.02ms -248us 0.998x PASS
ac_reads oltp_point_select 62.51ms 61.47ms -1.04ms 0.983x PASS
ac_reads oltp_range_select 16.88ms 16.75ms -129us 0.992x PASS
ac_reads oltp_sum_range 16.81ms 16.91ms +105us 1.006x PASS
ac_reads oltp_order_range 3.69ms 3.68ms -7us 0.998x PASS
ac_reads oltp_distinct_range 4.70ms 4.63ms -66us 0.986x PASS
ac_reads oltp_index_scan 9.05ms 9.04ms -12us 0.999x PASS
ac_reads select_random_points 24.24ms 25.10ms +856us 1.035x PASS
ac_reads select_random_ranges 7.69ms 7.70ms +7us 1.001x PASS
ac_reads covering_index_scan 7.36ms 7.25ms -109us 0.985x PASS
ac_reads groupby_scan 34.32ms 34.78ms +460us 1.013x PASS
ac_reads index_join 11.17ms 11.08ms -87us 0.992x PASS
ac_reads index_join_scan 5.88ms 5.86ms -21us 0.996x PASS
ac_reads types_table_scan 1.25s 1.27s +22.80ms 1.018x PASS
ac_reads table_scan 1.39s 1.38s -12.90ms 0.991x PASS
ac_reads oltp_read_only 174.67ms 173.94ms -735us 0.996x PASS
ac_writes oltp_bulk_insert_ac 88.23ms 88.25ms +18us 1.000x PASS
ac_writes oltp_insert_ac 106.72ms 108.81ms +2.09ms 1.020x PASS
ac_writes oltp_update_index_ac 126.18ms 117.43ms -8.76ms 0.931x PASS
ac_writes oltp_update_non_index_ac 103.32ms 97.03ms -6.29ms 0.939x PASS
ac_writes oltp_delete_insert_ac 113.08ms 114.69ms +1.61ms 1.014x PASS
ac_writes oltp_write_only_ac 113.66ms 111.13ms -2.53ms 0.978x PASS
ac_writes types_delete_insert_ac 99.09ms 97.71ms -1.38ms 0.986x PASS
ac_writes oltp_read_write_ac 118.18ms 119.85ms +1.68ms 1.014x PASS
compositepk details
Section Test Baseline Candidate Delta Ratio Result
mem_reads oltp_point_select 40.12ms 40.71ms +589us 1.015x PASS
mem_reads oltp_range_select 20.91ms 21.82ms +911us 1.044x PASS
mem_reads oltp_sum_range 20.76ms 20.94ms +180us 1.009x PASS
mem_reads oltp_order_range 3.85ms 3.90ms +41us 1.011x PASS
mem_reads oltp_distinct_range 4.88ms 5.00ms +124us 1.025x PASS
mem_reads oltp_index_scan 6.05ms 6.13ms +75us 1.012x PASS
mem_reads select_random_points 32.09ms 31.53ms -558us 0.983x PASS
mem_reads select_random_ranges 8.92ms 9.08ms +156us 1.017x PASS
mem_reads covering_index_scan 4.27ms 4.31ms +41us 1.010x PASS
mem_reads groupby_scan 38.93ms 39.01ms +84us 1.002x PASS
mem_reads index_join 10.55ms 10.59ms +39us 1.004x PASS
mem_reads index_join_scan 5.37ms 5.43ms +66us 1.012x PASS
mem_reads types_table_scan 1.26s 1.29s +29.30ms 1.023x PASS
mem_reads table_scan 1.41s 1.41s +4.87ms 1.003x PASS
mem_reads oltp_read_only 170.51ms 171.71ms +1.20ms 1.007x PASS
mem_writes oltp_bulk_insert 359.35ms 354.41ms -4.94ms 0.986x PASS
mem_writes oltp_insert 36.67ms 36.49ms -182us 0.995x PASS
mem_writes oltp_update_index 117.23ms 117.23ms +1us 1.000x PASS
mem_writes oltp_update_non_index 83.00ms 84.67ms +1.67ms 1.020x PASS
mem_writes oltp_delete_insert 96.47ms 96.49ms +20us 1.000x PASS
mem_writes oltp_write_only 57.63ms 57.70ms +75us 1.001x PASS
mem_writes types_delete_insert 55.54ms 55.12ms -419us 0.992x PASS
mem_writes oltp_read_write 156.99ms 156.97ms -24us 1.000x PASS
file_reads oltp_point_select 64.80ms 64.68ms -120us 0.998x PASS
file_reads oltp_range_select 23.80ms 24.57ms +765us 1.032x PASS
file_reads oltp_sum_range 23.65ms 23.80ms +143us 1.006x PASS
file_reads oltp_order_range 4.21ms 4.29ms +81us 1.019x PASS
file_reads oltp_distinct_range 5.28ms 5.41ms +122us 1.023x PASS
file_reads oltp_index_scan 9.02ms 9.11ms +91us 1.010x PASS
file_reads select_random_points 36.48ms 35.85ms -624us 0.983x PASS
file_reads select_random_ranges 11.72ms 11.79ms +71us 1.006x PASS
file_reads covering_index_scan 6.96ms 7.07ms +107us 1.015x PASS
file_reads groupby_scan 39.57ms 39.73ms +164us 1.004x PASS
file_reads index_join 12.65ms 12.86ms +211us 1.017x PASS
file_reads index_join_scan 5.91ms 5.87ms -35us 0.994x PASS
file_reads types_table_scan 1.25s 1.29s +39.53ms 1.032x PASS
file_reads table_scan 1.41s 1.40s -8.86ms 0.994x PASS
file_reads oltp_read_only 208.68ms 210.51ms +1.83ms 1.009x PASS
file_writes oltp_bulk_insert 379.47ms 378.61ms -861us 0.998x PASS
file_writes oltp_insert 46.24ms 46.48ms +238us 1.005x PASS
file_writes oltp_update_index 142.88ms 142.67ms -209us 0.999x PASS
file_writes oltp_update_non_index 103.54ms 103.78ms +245us 1.002x PASS
file_writes oltp_delete_insert 119.36ms 118.96ms -404us 0.997x PASS
file_writes oltp_write_only 77.99ms 77.50ms -496us 0.994x PASS
file_writes types_delete_insert 68.95ms 68.41ms -541us 0.992x PASS
file_writes oltp_read_write 179.49ms 176.12ms -3.37ms 0.981x PASS
ac_reads oltp_point_select 65.07ms 64.87ms -195us 0.997x PASS
ac_reads oltp_range_select 23.91ms 24.83ms +924us 1.039x PASS
ac_reads oltp_sum_range 23.97ms 23.79ms -184us 0.992x PASS
ac_reads oltp_order_range 4.25ms 4.31ms +69us 1.016x PASS
ac_reads oltp_distinct_range 5.26ms 5.40ms +137us 1.026x PASS
ac_reads oltp_index_scan 9.13ms 9.03ms -105us 0.989x PASS
ac_reads select_random_points 35.92ms 35.74ms -179us 0.995x PASS
ac_reads select_random_ranges 11.81ms 11.86ms +50us 1.004x PASS
ac_reads covering_index_scan 7.07ms 7.02ms -46us 0.993x PASS
ac_reads groupby_scan 39.42ms 39.82ms +399us 1.010x PASS
ac_reads index_join 12.68ms 12.80ms +115us 1.009x PASS
ac_reads index_join_scan 5.92ms 5.91ms -9us 0.998x PASS
ac_reads types_table_scan 1.25s 1.28s +34.13ms 1.027x PASS
ac_reads table_scan 1.41s 1.40s -14.85ms 0.989x PASS
ac_reads oltp_read_only 209.89ms 211.35ms +1.47ms 1.007x PASS
ac_writes oltp_bulk_insert_ac 78.49ms 78.77ms +281us 1.004x PASS
ac_writes oltp_insert_ac 98.06ms 97.67ms -390us 0.996x PASS
ac_writes oltp_update_index_ac 110.31ms 108.97ms -1.34ms 0.988x PASS
ac_writes oltp_update_non_index_ac 90.27ms 89.97ms -302us 0.997x PASS
ac_writes oltp_delete_insert_ac 100.62ms 102.39ms +1.77ms 1.018x PASS
ac_writes oltp_write_only_ac 101.10ms 101.11ms +19us 1.000x PASS
ac_writes types_delete_insert_ac 89.13ms 88.82ms -316us 0.996x PASS
ac_writes oltp_read_write_ac 108.98ms 107.49ms -1.49ms 0.986x PASS
int details
Section Test Baseline Candidate Delta Ratio Result
mem_reads oltp_point_select 28.74ms 27.45ms -1.29ms 0.955x PASS
mem_reads oltp_range_select 11.98ms 11.87ms -113us 0.991x PASS
mem_reads oltp_sum_range 11.41ms 11.39ms -19us 0.998x PASS
mem_reads oltp_order_range 2.88ms 2.86ms -20us 0.993x PASS
mem_reads oltp_distinct_range 3.88ms 3.89ms +1us 1.000x PASS
mem_reads oltp_index_scan 4.98ms 4.95ms -22us 0.996x PASS
mem_reads select_random_points 11.18ms 11.17ms -8us 0.999x PASS
mem_reads select_random_ranges 4.04ms 3.98ms -57us 0.986x PASS
mem_reads covering_index_scan 4.19ms 4.18ms -9us 0.998x PASS
mem_reads groupby_scan 34.56ms 34.59ms +38us 1.001x PASS
mem_reads index_join 7.76ms 7.73ms -27us 0.997x PASS
mem_reads index_join_scan 4.76ms 4.67ms -90us 0.981x PASS
mem_reads types_table_scan 1.26s 1.27s +4.52ms 1.004x PASS
mem_reads table_scan 1.38s 1.38s -695us 0.999x PASS
mem_reads oltp_read_only 116.63ms 115.44ms -1.19ms 0.990x PASS
mem_writes oltp_bulk_insert 251.21ms 242.64ms -8.57ms 0.966x PASS
mem_writes oltp_insert 28.43ms 28.15ms -275us 0.990x PASS
mem_writes oltp_update_index 107.33ms 106.72ms -617us 0.994x PASS
mem_writes oltp_update_non_index 60.11ms 59.40ms -713us 0.988x PASS
mem_writes oltp_delete_insert 81.25ms 80.54ms -708us 0.991x PASS
mem_writes oltp_write_only 45.82ms 44.90ms -913us 0.980x PASS
mem_writes types_delete_insert 40.87ms 39.77ms -1.10ms 0.973x PASS
mem_writes oltp_read_write 105.45ms 104.34ms -1.11ms 0.989x PASS
file_reads oltp_point_select 56.82ms 55.04ms -1.78ms 0.969x PASS
file_reads oltp_range_select 14.78ms 14.72ms -64us 0.996x PASS
file_reads oltp_sum_range 14.36ms 14.28ms -79us 0.994x PASS
file_reads oltp_order_range 3.22ms 3.20ms -15us 0.995x PASS
file_reads oltp_distinct_range 4.19ms 4.18ms -8us 0.998x PASS
file_reads oltp_index_scan 8.14ms 8.06ms -85us 0.990x PASS
file_reads select_random_points 13.99ms 14.06ms +67us 1.005x PASS
file_reads select_random_ranges 6.82ms 6.80ms -26us 0.996x PASS
file_reads covering_index_scan 7.28ms 7.11ms -177us 0.976x PASS
file_reads groupby_scan 35.31ms 34.58ms -731us 0.979x PASS
file_reads index_join 9.69ms 9.91ms +224us 1.023x PASS
file_reads index_join_scan 5.15ms 5.15ms -2us 1.000x PASS
file_reads types_table_scan 1.26s 1.26s +1.11ms 1.001x PASS
file_reads table_scan 1.37s 1.38s +6.35ms 1.005x PASS
file_reads oltp_read_only 155.94ms 154.63ms -1.31ms 0.992x PASS
file_writes oltp_bulk_insert 272.56ms 260.97ms -11.59ms 0.957x PASS
file_writes oltp_insert 36.13ms 35.76ms -375us 0.990x PASS
file_writes oltp_update_index 127.70ms 126.39ms -1.31ms 0.990x PASS
file_writes oltp_update_non_index 80.71ms 80.53ms -178us 0.998x PASS
file_writes oltp_delete_insert 97.97ms 96.86ms -1.11ms 0.989x PASS
file_writes oltp_write_only 64.77ms 64.19ms -588us 0.991x PASS
file_writes types_delete_insert 53.45ms 52.82ms -628us 0.988x PASS
file_writes oltp_read_write 124.82ms 124.05ms -775us 0.994x PASS
ac_reads oltp_point_select 56.09ms 55.19ms -899us 0.984x PASS
ac_reads oltp_range_select 14.79ms 14.75ms -42us 0.997x PASS
ac_reads oltp_sum_range 14.39ms 14.32ms -70us 0.995x PASS
ac_reads oltp_order_range 3.20ms 3.19ms -3us 0.999x PASS
ac_reads oltp_distinct_range 4.20ms 4.21ms +1us 1.000x PASS
ac_reads oltp_index_scan 8.09ms 7.99ms -99us 0.988x PASS
ac_reads select_random_points 14.13ms 14.08ms -54us 0.996x PASS
ac_reads select_random_ranges 6.86ms 6.85ms -10us 0.999x PASS
ac_reads covering_index_scan 7.21ms 7.03ms -174us 0.976x PASS
ac_reads groupby_scan 34.96ms 34.52ms -439us 0.987x PASS
ac_reads index_join 9.97ms 9.82ms -149us 0.985x PASS
ac_reads index_join_scan 5.10ms 5.17ms +65us 1.013x PASS
ac_reads types_table_scan 1.25s 1.26s +8.26ms 1.007x PASS
ac_reads table_scan 1.37s 1.37s -244us 1.000x PASS
ac_reads oltp_read_only 157.07ms 155.70ms -1.38ms 0.991x PASS
ac_writes oltp_bulk_insert_ac 59.52ms 61.29ms +1.78ms 1.030x PASS
ac_writes oltp_insert_ac 80.95ms 82.83ms +1.88ms 1.023x PASS
ac_writes oltp_update_index_ac 99.02ms 93.79ms -5.23ms 0.947x PASS
ac_writes oltp_update_non_index_ac 71.04ms 74.69ms +3.65ms 1.051x PASS
ac_writes oltp_delete_insert_ac 88.48ms 84.84ms -3.63ms 0.959x PASS
ac_writes oltp_write_only_ac 83.35ms 86.06ms +2.71ms 1.032x PASS
ac_writes types_delete_insert_ac 76.75ms 74.52ms -2.24ms 0.971x PASS
ac_writes oltp_read_write_ac 89.98ms 94.53ms +4.55ms 1.051x PASS
textpk details
Section Test Baseline Candidate Delta Ratio Result
mem_reads oltp_point_select 35.15ms 34.55ms -600us 0.983x PASS
mem_reads oltp_range_select 13.53ms 13.62ms +92us 1.007x PASS
mem_reads oltp_sum_range 13.21ms 13.22ms +17us 1.001x PASS
mem_reads oltp_order_range 3.15ms 3.15ms +2us 1.001x PASS
mem_reads oltp_distinct_range 4.16ms 4.17ms +9us 1.002x PASS
mem_reads oltp_index_scan 5.97ms 5.99ms +15us 1.003x PASS
mem_reads select_random_points 20.18ms 19.97ms -204us 0.990x PASS
mem_reads select_random_ranges 5.26ms 5.22ms -44us 0.992x PASS
mem_reads covering_index_scan 4.34ms 4.37ms +22us 1.005x PASS
mem_reads groupby_scan 35.42ms 35.59ms +162us 1.005x PASS
mem_reads index_join 8.82ms 8.76ms -68us 0.992x PASS
mem_reads index_join_scan 5.70ms 5.64ms -56us 0.990x PASS
mem_reads types_table_scan 1.24s 1.25s +9.10ms 1.007x PASS
mem_reads table_scan 1.35s 1.36s +6.44ms 1.005x PASS
mem_reads oltp_read_only 128.46ms 129.00ms +539us 1.004x PASS
mem_writes oltp_bulk_insert 337.38ms 336.97ms -410us 0.999x PASS
mem_writes oltp_insert 39.49ms 39.30ms -195us 0.995x PASS
mem_writes oltp_update_index 135.74ms 135.38ms -363us 0.997x PASS
mem_writes oltp_update_non_index 87.96ms 87.87ms -89us 0.999x PASS
mem_writes oltp_delete_insert 106.43ms 105.57ms -853us 0.992x PASS
mem_writes oltp_write_only 63.95ms 63.69ms -267us 0.996x PASS
mem_writes types_delete_insert 54.86ms 54.65ms -211us 0.996x PASS
mem_writes oltp_read_write 135.83ms 135.54ms -288us 0.998x PASS
file_reads oltp_point_select 64.31ms 63.81ms -496us 0.992x PASS
file_reads oltp_range_select 16.77ms 16.82ms +44us 1.003x PASS
file_reads oltp_sum_range 16.63ms 16.57ms -62us 0.996x PASS
file_reads oltp_order_range 3.56ms 3.56ms +7us 1.002x PASS
file_reads oltp_distinct_range 4.57ms 4.54ms -31us 0.993x PASS
file_reads oltp_index_scan 9.18ms 9.29ms +112us 1.012x PASS
file_reads select_random_points 23.51ms 23.55ms +39us 1.002x PASS
file_reads select_random_ranges 8.20ms 8.18ms -24us 0.997x PASS
file_reads covering_index_scan 7.76ms 7.75ms -6us 0.999x PASS
file_reads groupby_scan 36.09ms 35.92ms -167us 0.995x PASS
file_reads index_join 11.40ms 11.32ms -83us 0.993x PASS
file_reads index_join_scan 6.30ms 6.17ms -136us 0.978x PASS
file_reads types_table_scan 1.23s 1.24s +12.52ms 1.010x PASS
file_reads table_scan 1.36s 1.35s -2.26ms 0.998x PASS
file_reads oltp_read_only 170.45ms 170.89ms +447us 1.003x PASS
file_writes oltp_bulk_insert 372.06ms 361.23ms -10.83ms 0.971x PASS
file_writes oltp_insert 52.96ms 52.55ms -404us 0.992x PASS
file_writes oltp_update_index 173.36ms 172.24ms -1.12ms 0.994x PASS
file_writes oltp_update_non_index 114.34ms 114.47ms +131us 1.001x PASS
file_writes oltp_delete_insert 136.69ms 136.10ms -588us 0.996x PASS
file_writes oltp_write_only 87.68ms 87.01ms -670us 0.992x PASS
file_writes types_delete_insert 74.68ms 74.09ms -586us 0.992x PASS
file_writes oltp_read_write 160.06ms 159.24ms -824us 0.995x PASS
ac_reads oltp_point_select 64.85ms 63.46ms -1.39ms 0.979x PASS
ac_reads oltp_range_select 16.76ms 16.68ms -75us 0.996x PASS
ac_reads oltp_sum_range 16.75ms 16.49ms -260us 0.984x PASS
ac_reads oltp_order_range 3.52ms 3.53ms +10us 1.003x PASS
ac_reads oltp_distinct_range 4.54ms 4.53ms -2us 1.000x PASS
ac_reads oltp_index_scan 9.18ms 9.17ms -12us 0.999x PASS
ac_reads select_random_points 23.42ms 23.43ms +14us 1.001x PASS
ac_reads select_random_ranges 8.26ms 8.15ms -108us 0.987x PASS
ac_reads covering_index_scan 7.72ms 7.71ms -16us 0.998x PASS
ac_reads groupby_scan 36.05ms 35.97ms -85us 0.998x PASS
ac_reads index_join 11.33ms 11.31ms -18us 0.998x PASS
ac_reads index_join_scan 6.26ms 6.17ms -89us 0.986x PASS
ac_reads types_table_scan 1.24s 1.25s +12.45ms 1.010x PASS
ac_reads table_scan 1.35s 1.35s +3.36ms 1.002x PASS
ac_reads oltp_read_only 171.59ms 170.54ms -1.05ms 0.994x PASS
ac_writes oltp_bulk_insert_ac 61.72ms 66.49ms +4.77ms 1.077x PASS
ac_writes oltp_insert_ac 86.52ms 77.25ms -9.26ms 0.893x PASS
ac_writes oltp_update_index_ac 95.55ms 101.95ms +6.40ms 1.067x PASS
ac_writes oltp_update_non_index_ac 79.65ms 73.26ms -6.39ms 0.920x PASS
ac_writes oltp_delete_insert_ac 85.13ms 94.60ms +9.47ms 1.111x PASS
ac_writes oltp_write_only_ac 90.43ms 83.21ms -7.22ms 0.920x PASS
ac_writes types_delete_insert_ac 73.28ms 78.65ms +5.36ms 1.073x PASS
ac_writes oltp_read_write_ac 99.56ms 92.93ms -6.64ms 0.933x PASS
vc details
Section Test Baseline Candidate Delta Ratio Result
vc status_clean_many_tables 89.40ms 89.97ms +565us 1.006x PASS
vc status_dirty_many_tables 92.47ms 92.96ms +490us 1.005x PASS
vc diff_regular_working_one_table 85.16ms 84.98ms -181us 0.998x PASS
vc diff_regular_working_many_tables 98.04ms 98.59ms +545us 1.006x PASS
vc diff_stat_working_many_tables 97.98ms 98.69ms +704us 1.007x PASS
vc diff_schema_working_many_tables 98.17ms 99.03ms +864us 1.009x PASS
vc branch_list_many_branches 23.29ms 23.27ms -19us 0.999x PASS
vc branch_create_delete 25.41ms 25.56ms +145us 1.006x PASS
vc checkout_branch_clean 57.77ms 58.13ms +360us 1.006x PASS
vc merge_data_no_conflicts 30.26ms 30.14ms -122us 0.996x PASS
vc merge_schema_no_conflicts 22.69ms 22.39ms -305us 0.987x PASS
vc merge_data_conflicts 128.51ms 128.65ms +145us 1.001x PASS
vc merge_data_conflicts_with_resolve 128.43ms 128.10ms -334us 0.997x PASS

All relative performance gates passed.

@itoqa

itoqa Bot commented Aug 6, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: adbbb61: 18 test cases ran, 18 passed ✅.

Summary

Coverage focused on safe completion and cancellation of repository rebase operations, including concurrent requests, stale connections, reopening, cleanup, error recovery, malformed state, and bounded retries. Both normal workflows and adversarial timing and fault conditions remained healthy, with the repository consistently usable for later commits.

Safe to merge — the exercised rebase, cleanup, concurrency, persistence, and error-handling behaviors showed no PR-attributable regressions or failures. A small number of specialized fault scenarios were not exercised, but they are coverage caveats rather than merge blockers.

Tests run by Ito

View full run

Result Severity Type Description
Cleanup Aborting the reopened rebase completed successfully even after the temporary branch had already been removed. The plan and durable rebase state were cleared, and the repository accepted a later commit.
Cleanup The concurrent cleanup check passed all 12 trials. A peer commit stayed intact, temporary rebase state was removed, and a later commit succeeded.
Cleanup The rebase cleanup completed successfully. After reopening the database, the old temporary branch was still gone and normal repository use remained possible.
Cleanup The abort and continue checks returned a recovery error when temporary-branch deletion failed for a reason other than a missing branch. The repository did not report that cleanup had succeeded.
Contract The local continue-versus-abort race completed all 44 checks successfully. Across 12 trials, exactly one side completed each terminal operation, no recovery failure was reported, and the database accepted a later commit.
Contract Ending a rebase from competing connections completed cleanly. The local checks reported 44 passed, 0 failed, and no invalid terminal result.
Contract The rebase race completed all 44 checks successfully. Continue and abort never both won, and the database stayed usable for a later commit.
Contract Concurrent continue and abort operations finished with one valid winner in every trial, and the repository accepted a new commit after each race.
Contract Plan-read, invalid-plan, conflict recovery, and recovery-failure checks all passed. The repository kept its durable state and returned the expected error category in all 50 assertions.
Contract The rebase regression checks passed in all 12 peer-commit trials and all 44 concurrency checks. Only one terminal operation was accepted in each race, no duplicate winner was reported, and a follow-up commit still worked.
Durable The local multi-process rebase regression completed successfully with 44 checks passed and 0 failures. The rebuilt database test covered durable rebase state across concurrent terminal operations and exited successfully.
Durable Continuing with a malformed rebase plan returned the expected schema error before changing the rebase. The malformed plan, active working branch, and recovery state remained available.
Durable The available local checks completed successfully, and no user-visible rebase failure was found. The requested plan cleanup failure could not be injected through the available test runner, so the fault-specific behavior remains unexercised.
Durable The available concurrency checks passed, and no stale rebase state was shown to replay after another operation finished. The specific pause-and-remove scenario could not be run because the local test setup has no synchronization hook before the rebase claim.
Rebase Continue finished the rebase successfully, cleared the rebase state, removed the temporary branch, and allowed a later commit to succeed.
Rebase Abort and continue raced across 12 trials. One operation won each time, the other reported no active rebase, and a later commit still succeeded.
Rebase A connection left open after another connection finished the rebase could not replay or undo the result. The follow-up commit checks also passed.
Rebase The rebase regression finished in 3.317 seconds under the 10-second limit. All 44 checks passed, with no bad outcomes, and the database remained usable for later work.

Tip

Reply with @itoqa to send us feedback on this test run.

Concurrent --continue can leave abort's best-effort cleanup racy after
isRebasing is already cleared. Once the claim wins, report
"Interactive rebase aborted" regardless of cleanup noise. On claim
hard-failure, re-check durable state and prefer "no rebase in progress"
when a peer already ended the rebase.
@itoqa

itoqa Bot commented Aug 6, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Reportadbbb619c289df: 21 test cases ran, 2 regressions ❌, 1 new failure ❌, 17 passing ✅, 1 additional finding ⚠️.

Diff Summary

Coverage spans normal interactive rebase completion and abort flows, durable state across reopen, concurrent terminal operations, cleanup safety, branch preservation, conflict handling, and adversarial storage-failure cases. The normal and concurrency behavior is broadly healthy, but recovery-error reporting remains unreliable when abort cleanup or state verification fails.

Not safe to merge yet — this PR has multiple attributable regressions and new failures in abort recovery handling that can hide storage or state-refresh errors while leaving cleanup incomplete or the repository state uncertain. An unrelated pre-existing recovery-reporting issue is a flag for later, but the concentration of PR-related failures makes this a merge blocker.

Tests run by Ito

View full run

Result State Severity Type Description
🆕 Regression Medium severity Cleanup The injected cleanup error was reproduced by the native regression harness, but abort returned "Interactive rebase aborted" instead of the expected "rebase recovery failed" error. The harness recorded 11 passing checks and one failed assertion for this recovery-failure message.
🆕 Regression Medium severity Contract The abort operation hit the injected storage failure, but the result did not contain the expected recovery error. The test's recovery-failure assertion failed because the operation instead treated the cleared durable state as proof that no rebase was in progress.
❌ New Failure Medium severity Durable The recovery regression completed 11 of 12 checks, but rebase_abort_recovery_failure_is_returned failed. The abort operation did not return the required 'ERROR: rebase recovery failed' result.
Passing Claim When the database could not be opened, abort returned an error and did not claim the rebase. After the database was restored, the active plan and temporary branch were still present.
Passing Claim Concurrent rebase cleanup completed without errors, and a fresh connection could still use the database afterward.
Passing Claim Concurrent rebase commands completed successfully. The losing command did not leave the database locked, and a new connection could continue using it.
Passing Claim After repeated terminal operations compete for the graph lock, the shared database can be reopened and a new commit succeeds.
Passing Cleanup Aborting after another connection removed the temporary branch completed successfully. The repository stayed usable, and a new commit could still be made.
Passing Cleanup Aborting the rebase did not overwrite an independent change on the return branch. The local regression suite completed all 24 checks successfully.
Passing Cleanup After terminal cleanup removed the temporary branch, reopening the local database did not bring it back. Follow-up continue, abort, and commit operations remained safe, with all 44 checks passing.
Passing Cleanup Aborting an interactive rebase returned success, left the repository on the expected branch, and removed the rebase plan and temporary branch.
Passing Contract The repository handled all 12 continue-versus-abort races successfully. Every trial completed without a recovery error, hang, or lost child result, and the harness finished all 44 assertions.
Passing Contract Running abort and continue in either order completed the first operation and made the second report that no rebase was in progress. The suite passed all 24 assertions and left no stale plan or temporary branch.
Passing Contract Concurrent rebase cleanup completed successfully, and every trial allowed a new row to be inserted and committed afterward.
Passing Contract The cleanup and reopen checks passed all 47 assertions. Abort returned success after a plan-read failure, restored durable state after a conflict, and left no stale active rebase after reopening the database.
Passing Durable The concurrent continue and abort checks completed all 44 checks. Across 12 races, exactly one operation won, the loser did not corrupt the rebase state, and the final repository stayed usable.
Passing Durable A replay conflict stops the operation once, clears the old rebase state, and restores the repository. Retrying from a fresh connection does not replay the cleared work, and a later commit remains possible.
Passing Durable Reopening the database preserves the active rebase until it is safely aborted, malformed plan schemas stay blocked, and plan-read errors do not leave partial state. All 42 checks passed across the three regression tests.
Passing Rebase Abort and continue were run at the same time across 12 trials. One operation won each trial, the losing operation did not report recovery failure, and the repository accepted a new commit afterward.
Passing Rebase The local rebase regression checks passed all 24 cases, including terminal cleanup after reopening the database. No replay or branch change was observed after rebase completion, although the shell suite did not directly exercise two live stale connections.
⏸️ Skipped Contract The rebase race completed all 44 checks successfully. Continue and abort never both won, and the database stayed usable for a later commit.
⏸️ Skipped Contract The rebase regression checks passed in all 12 peer-commit trials and all 44 concurrency checks. Only one terminal operation was accepted in each race, no duplicate winner was reported, and a follow-up commit still worked.
⏸️ Skipped Durable The local multi-process rebase regression completed successfully with 44 checks passed and 0 failures. The rebuilt database test covered durable rebase state across concurrent terminal operations and exited successfully.
⏸️ Skipped Durable Continuing with a malformed rebase plan returned the expected schema error before changing the rebase. The malformed plan, active working branch, and recovery state remained available.
⏸️ Skipped Rebase Continue finished the rebase successfully, cleared the rebase state, removed the temporary branch, and allowed a later commit to succeed.
⏸️ Skipped Rebase The rebase regression finished in 3.317 seconds under the 10-second limit. All 44 checks passed, with no bad outcomes, and the database remained usable for later work.
⚠️ Additional Finding Medium severity Durable Starting an abort with a storage error shows a generic rebase failure instead of the recovery failure message.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Abort hides a rebase recovery failure
  • Severity: Medium Medium severity
  • Description: Starting an abort with a storage error shows a generic rebase failure instead of the recovery failure message.
  • Impact: During an abort, a storage failure is reported as a generic rebase error instead of a recovery failure. Users may not know that recovery is incomplete and may take the wrong next steps.
  • Steps to Reproduce:
    1. Create a repository with an active interactive rebase from feat onto main.
    2. Inject fault 953 so the DROP TABLE operation used while claiming the end of the rebase returns an I/O error.
    3. Run SELECT dolt_rebase('--abort').
    4. Check the returned error text and confirm that it says rebase recovery failed rather than only rebase failed.
  • Stub / mock content: Fault 953 was intentionally injected to simulate a DROP TABLE I/O failure during the targeted recovery regression; no application mocks, route intercepts, or bypasses were used.
  • Code Analysis: rebaseClaimActiveEnd in src/doltlite_rebase.c:1070-1083 first clears and saves the session rebase state, then calls sqlite3FaultSim(953) at line 1081 and converts the simulated DROP TABLE failure into SQLITE_IOERR at line 1083. doltliteRebaseInteractiveAbort calls rebaseClaimActiveEndRetry at lines 1542-1544, but it sets bPlanDropped only after SQLITE_OK at line 1550. The injected SQLITE_IOERR therefore takes the abort_err branch with bPlanDropped still zero. Lines 1640-1647 then free the branch names and return the generic "rebase failed" message without calling rebaseResultRecoveryFailure. The regression oracle at test/doltlite_regression_test_c.c:6948-6959 requires "ERROR: rebase recovery failed" and rejects a success message, so the implementation and oracle disagree deterministically. The smallest fix is to preserve the fact that durable termination was claimed before the DROP TABLE failure, or otherwise route this post-clear failure through the recovery-failure path instead of the pre-claim generic-error branch. The PR diff only changes the later post-claim cleanup handling around src/doltlite_rebase.c:1411 and does not change this claim/error path.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread src/doltlite_rebase.c

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

🔁 Regression: previously passing at adbbb61

Medium severity Abort hides cleanup failures

What failed: The injected cleanup error was reproduced by the native regression harness, but abort returned "Interactive rebase aborted" instead of the expected "rebase recovery failed" error. The harness recorded 11 passing checks and one failed assertion for this recovery-failure message.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: When a storage error interrupts abort cleanup, users are told the rebase was aborted successfully even though cleanup may be incomplete. They may need to retry or manually recover the repository state.
  • Steps to Reproduce:
    1. Create a repository with main and feat branches, then start an interactive rebase on feat.
    2. Inject a non-SQLITE_NOTFOUND error while abort removes the temporary rebase branch.
    3. Run the interactive rebase abort operation and capture its result.
    4. Check the temporary branch and working-set state after the operation.
  • Stub / mock content: The native regression harness used SQLite fault injection to simulate a non-NOTFOUND ref deletion error. No application routes, external services, or production data were used.
  • Code Analysis: The cleanup helper rebaseCleanupAfterClaim in src/doltlite_rebase.c:1101-1127 preserves the first error from doltliteMutateRefs and doltlitePersistWorkingSet, so a ref deletion error other than SQLITE_NOTFOUND remains a failure. The only allowed idempotent exception is explicit in rebaseDeleteWorkingBranchRefs at src/doltlite_rebase.c:1021-1027, where SQLITE_NOTFOUND is converted to SQLITE_OK. In the PR-changed abort path at src/doltlite_rebase.c:1434-1442, the return value from rebaseCleanupAfterClaim is explicitly cast to void, and the return values from rebaseRestoreReturnBranchWorkingState, doltlitePersistWorkingSet, and doltliteVcSealBranchStyleTxn are also discarded before src/doltlite_rebase.c:1447 always returns "Interactive rebase aborted". The regression test in test/doltlite_regression_test_c.c:6948-6959 installs fault code 953 for the abort cleanup path and requires an ERROR: rebase recovery failed result; that assertion failed. The smallest practical fix is to retain the cleanup return code after the durable claim, return rebaseResultRecoveryFailure when a non-NOTFOUND cleanup error occurs, and continue treating only SQLITE_NOTFOUND as successful idempotent cleanup. The post-claim claim ownership should remain durable, so this fix only restores truthful error reporting rather than undoing the claim.
  • Why this is likely a bug: This is not only a runtime symptom: the source records non-NOTFOUND cleanup errors, while the abort caller deliberately throws that result away and unconditionally emits a success message. The native fault-injection test directly exercises the error path and fails exactly where the public error contract requires recovery failure. A real I/O or storage error can therefore be hidden from the caller, leaving cleanup incomplete while presenting a successful terminal result. The PR diff directly introduced the discarded cleanup-result behavior at src/doltlite_rebase.c:1434-1442, so the smallest fix is to propagate that retained error while preserving the intended success behavior for a missing temporary ref.
Relevant code

src/doltlite_rebase.c:1021-1027

rc = chunkStoreDeleteBranch(cs, zWorkingBranch);
return rc==SQLITE_NOTFOUND ? SQLITE_OK : rc;

src/doltlite_rebase.c:1104-1127

static int rebaseCleanupAfterClaim(
  sqlite3 *db,
  const char *zOrigBranch,
  const char *zWorkingBranch
){
  ...
  rebaseKeepFirstError(&rc, rc2);
  ...
  return rc;
}

src/doltlite_rebase.c:1434-1447

(void)rebaseCleanupAfterClaim(db, zOrigBranch, zWorking);
...
(void)doltlitePersistWorkingSet(db);
(void)doltliteVcSealBranchStyleTxn(db);
...
sqlite3_result_text(context, "Interactive rebase aborted", -1, SQLITE_STATIC);

test/doltlite_regression_test_c.c:6948-6959

gRegressionFaultCode = 953;
...
res = queryScalarText(db, "SELECT dolt_rebase('--abort')");
...
check("rebase_abort_recovery_failure_is_returned",
      strstr(res, "ERROR: rebase recovery failed")!=0);
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Abort hides cleanup failures**

**What failed:** The injected cleanup error was reproduced by the native regression harness, but abort returned "Interactive rebase aborted" instead of the expected "rebase recovery failed" error. The harness recorded 11 passing checks and one failed assertion for this recovery-failure message.

- **Impact:** When a storage error interrupts abort cleanup, users are told the rebase was aborted successfully even though cleanup may be incomplete. They may need to retry or manually recover the repository state.
- **Steps to reproduce:**
  1. Create a repository with main and feat branches, then start an interactive rebase on feat.
  2. Inject a non-SQLITE_NOTFOUND error while abort removes the temporary rebase branch.
  3. Run the interactive rebase abort operation and capture its result.
  4. Check the temporary branch and working-set state after the operation.
- **Stub / mock content:** The native regression harness used SQLite fault injection to simulate a non-NOTFOUND ref deletion error. No application routes, external services, or production data were used.
- **Code analysis:** The cleanup helper rebaseCleanupAfterClaim in src/doltlite_rebase.c:1101-1127 preserves the first error from doltliteMutateRefs and doltlitePersistWorkingSet, so a ref deletion error other than SQLITE_NOTFOUND remains a failure. The only allowed idempotent exception is explicit in rebaseDeleteWorkingBranchRefs at src/doltlite_rebase.c:1021-1027, where SQLITE_NOTFOUND is converted to SQLITE_OK. In the PR-changed abort path at src/doltlite_rebase.c:1434-1442, the return value from rebaseCleanupAfterClaim is explicitly cast to void, and the return values from rebaseRestoreReturnBranchWorkingState, doltlitePersistWorkingSet, and doltliteVcSealBranchStyleTxn are also discarded before src/doltlite_rebase.c:1447 always returns "Interactive rebase aborted". The regression test in test/doltlite_regression_test_c.c:6948-6959 installs fault code 953 for the abort cleanup path and requires an ERROR: rebase recovery failed result; that assertion failed. The smallest practical fix is to retain the cleanup return code after the durable claim, return rebaseResultRecoveryFailure when a non-NOTFOUND cleanup error occurs, and continue treating only SQLITE_NOTFOUND as successful idempotent cleanup. The post-claim claim ownership should remain durable, so this fix only restores truthful error reporting rather than undoing the claim.
- **Why this is likely a bug:** This is not only a runtime symptom: the source records non-NOTFOUND cleanup errors, while the abort caller deliberately throws that result away and unconditionally emits a success message. The native fault-injection test directly exercises the error path and fails exactly where the public error contract requires recovery failure. A real I/O or storage error can therefore be hidden from the caller, leaving cleanup incomplete while presenting a successful terminal result. The PR diff directly introduced the discarded cleanup-result behavior at src/doltlite_rebase.c:1434-1442, so the smallest fix is to propagate that retained error while preserving the intended success behavior for a missing temporary ref.

**Relevant code:**

`src/doltlite_rebase.c:1021-1027`

~~~c
rc = chunkStoreDeleteBranch(cs, zWorkingBranch);
return rc==SQLITE_NOTFOUND ? SQLITE_OK : rc;
~~~

`src/doltlite_rebase.c:1104-1127`

~~~c
static int rebaseCleanupAfterClaim(
  sqlite3 *db,
  const char *zOrigBranch,
  const char *zWorkingBranch
){
  ...
  rebaseKeepFirstError(&rc, rc2);
  ...
  return rc;
}
~~~

`src/doltlite_rebase.c:1434-1447`

~~~c
(void)rebaseCleanupAfterClaim(db, zOrigBranch, zWorking);
...
(void)doltlitePersistWorkingSet(db);
(void)doltliteVcSealBranchStyleTxn(db);
...
sqlite3_result_text(context, "Interactive rebase aborted", -1, SQLITE_STATIC);
~~~

`test/doltlite_regression_test_c.c:6948-6959`

~~~c
gRegressionFaultCode = 953;
...
res = queryScalarText(db, "SELECT dolt_rebase('--abort')");
...
check("rebase_abort_recovery_failure_is_returned",
      strstr(res, "ERROR: rebase recovery failed")!=0);
~~~

Comment thread src/doltlite_rebase.c
goto claim_done;
}

rc = doltliteClearSessionRebaseState(db);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

🔁 Regression: previously passing at adbbb61

Medium severity Abort hides rebase recovery errors

What failed: The abort operation hit the injected storage failure, but the result did not contain the expected recovery error. The test's recovery-failure assertion failed because the operation instead treated the cleared durable state as proof that no rebase was in progress.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: When an abort encounters a storage failure, users are told that no rebase is in progress instead of being warned that recovery failed. Rebase state can remain behind, requiring manual recovery.
  • Steps to Reproduce:
    1. Create a repository with main and feat branches, then start an interactive rebase from feat onto main.
    2. Inject storage fault 953 while the abort operation drops the rebase plan after clearing durable rebase state.
    3. Run the rebase abort operation and inspect its returned error text.
    4. Compare the result with the expected recovery-failure message and confirm it does not report successful abort completion.
  • Stub / mock content: The test used an intentional native storage-fault injection to exercise recovery handling; no application mocks, route stubs, or source bypasses were applied.
  • Code Analysis: In /tmp/output-agent-workspace/repo/src/doltlite_rebase.c, rebaseClaimActiveEnd() clears and persists the durable rebase state at lines 1070-1073, then calls sqlite3FaultSim(953) before DROP TABLE at lines 1080-1083. That makes fault 953 a claim-adjacent error: the rebase flag is already cleared, but removing main.dolt_rebase has failed. The PR diff changes doltliteRebaseInteractiveAbort() at lines 1413-1429. After any non-OK claim result, it force-refreshes and reloads the working set, then returns no rebase in progress when stillRebasing is false. Because fault 953 occurs after the durable flag was cleared, this new branch suppresses rebaseResultRecoveryFailure(context, rc), which is the required error path. The smallest fix is to distinguish a peer-lost claim (SQLITE_DONE) from a claim error after local durable termination began, or otherwise preserve the claim error for fault 953 instead of converting every refreshed clear flag into no rebase.
  • Why this is likely a bug: The targeted native test passed 11 of 12 assertions and confirmed that fault 953 was injected; only the required recovery-error category failed. The source path explains the exact mismatch without relying on the unavailable browser service: the changed abort handler uses the post-failure cleared flag as a peer-loss signal even when this same operation cleared it and then failed to drop the plan. This can hide a real storage/recovery failure and leave main.dolt_rebase behind, so it is a production error-handling defect rather than test setup noise. A targeted fix should preserve recovery failure for errors returned after local durable claim work, while retaining no rebase in progress for a genuine SQLITE_DONE peer win.
Relevant code

src/doltlite_rebase.c:1070-1083

rc = doltliteClearSessionRebaseState(db); ... rc = doltliteSaveWorkingSet(db); ... rcDrop = sqlite3FaultSim(953) ? SQLITE_IOERR : sqlite3_exec(db, "DROP TABLE IF EXISTS main.dolt_rebase", 0, 0, 0);

src/doltlite_rebase.c:1413-1429

if( rc!=SQLITE_OK ){ ... doltliteGetSessionRebaseState(db, &stillRebasing, 0, 0, 0, 0); ... if( !stillRebasing ) sqlite3_result_error(context, "no rebase in progress", -1); else rebaseResultRecoveryFailure(context, rc); }

test/doltlite_regression_test_c.c:6948-6959

gRegressionFaultCode = 953; ... res = queryScalarText(db, "SELECT dolt_rebase('--abort')"); ... check("rebase_abort_recovery_failure_is_returned", strstr(res, "ERROR: rebase recovery failed")!=0);
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Abort hides rebase recovery errors**

**What failed:** The abort operation hit the injected storage failure, but the result did not contain the expected recovery error. The test's recovery-failure assertion failed because the operation instead treated the cleared durable state as proof that no rebase was in progress.

- **Impact:** When an abort encounters a storage failure, users are told that no rebase is in progress instead of being warned that recovery failed. Rebase state can remain behind, requiring manual recovery.
- **Steps to reproduce:**
  1. Create a repository with main and feat branches, then start an interactive rebase from feat onto main.
  2. Inject storage fault 953 while the abort operation drops the rebase plan after clearing durable rebase state.
  3. Run the rebase abort operation and inspect its returned error text.
  4. Compare the result with the expected recovery-failure message and confirm it does not report successful abort completion.
- **Stub / mock content:** The test used an intentional native storage-fault injection to exercise recovery handling; no application mocks, route stubs, or source bypasses were applied.
- **Code analysis:** In /tmp/output-agent-workspace/repo/src/doltlite_rebase.c, rebaseClaimActiveEnd() clears and persists the durable rebase state at lines 1070-1073, then calls sqlite3FaultSim(953) before DROP TABLE at lines 1080-1083. That makes fault 953 a claim-adjacent error: the rebase flag is already cleared, but removing main.dolt_rebase has failed. The PR diff changes doltliteRebaseInteractiveAbort() at lines 1413-1429. After any non-OK claim result, it force-refreshes and reloads the working set, then returns no rebase in progress when stillRebasing is false. Because fault 953 occurs after the durable flag was cleared, this new branch suppresses rebaseResultRecoveryFailure(context, rc), which is the required error path. The smallest fix is to distinguish a peer-lost claim (SQLITE_DONE) from a claim error after local durable termination began, or otherwise preserve the claim error for fault 953 instead of converting every refreshed clear flag into no rebase.
- **Why this is likely a bug:** The targeted native test passed 11 of 12 assertions and confirmed that fault 953 was injected; only the required recovery-error category failed. The source path explains the exact mismatch without relying on the unavailable browser service: the changed abort handler uses the post-failure cleared flag as a peer-loss signal even when this same operation cleared it and then failed to drop the plan. This can hide a real storage/recovery failure and leave main.dolt_rebase behind, so it is a production error-handling defect rather than test setup noise. A targeted fix should preserve recovery failure for errors returned after local durable claim work, while retaining no rebase in progress for a genuine SQLITE_DONE peer win.

**Relevant code:**

`src/doltlite_rebase.c:1070-1083`

~~~c
rc = doltliteClearSessionRebaseState(db); ... rc = doltliteSaveWorkingSet(db); ... rcDrop = sqlite3FaultSim(953) ? SQLITE_IOERR : sqlite3_exec(db, "DROP TABLE IF EXISTS main.dolt_rebase", 0, 0, 0);
~~~

`src/doltlite_rebase.c:1413-1429`

~~~c
if( rc!=SQLITE_OK ){ ... doltliteGetSessionRebaseState(db, &stillRebasing, 0, 0, 0, 0); ... if( !stillRebasing ) sqlite3_result_error(context, "no rebase in progress", -1); else rebaseResultRecoveryFailure(context, rc); }
~~~

`test/doltlite_regression_test_c.c:6948-6959`

~~~c
gRegressionFaultCode = 953; ... res = queryScalarText(db, "SELECT dolt_rebase('--abort')"); ... check("rebase_abort_recovery_failure_is_returned", strstr(res, "ERROR: rebase recovery failed")!=0);
~~~

Comment thread src/doltlite_rebase.c
zBranch = doltliteGetSessionBranch(db);
if( !zBranch || !zBranch[0] ) zBranch = "main";

rc = chunkStoreLockAndRefresh(cs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

🆕 New Failure: identified in this diff run

Medium severity Abort hides refresh failures during recovery

What failed: The recovery regression completed 11 of 12 checks, but rebase_abort_recovery_failure_is_returned failed. The abort operation did not return the required 'ERROR: rebase recovery failed' result.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: When a rebase abort cannot verify durable state, users receive a generic failure instead of a recovery-failure message. They may not know that the rebase state is unverified and needs recovery guidance.
  • Steps to Reproduce:
    1. Create a repository with main and feat branches and start an interactive rebase from feat onto main.
    2. Inject a non-BUSY claim error followed by a force-refresh or durable working-set reload failure.
    3. Run dolt_rebase('--abort').
    4. Check the returned error and the persisted isRebasing state.
  • Stub / mock content: The test used controlled SQLite fault injection to model a hard claim and refresh failure. No production services, customer data, or application-route mocks were used.
  • Code Analysis: The new claim path in rebaseClaimActiveEnd() calls chunkStoreLockAndRefresh(), chunkStoreForceRefresh(), and doltliteLoadWorkingSet() at src/doltlite_rebase.c:1057-1063. Any non-OK result exits before the durable claim completes. The caller then handles the claim result at src/doltlite_rebase.c:1404-1447, where the PR added a refresh-and-reload check before deciding whether the peer won; if those checks cannot read durable state, the code must retain the original claim error and call rebaseResultRecoveryFailure(), not downgrade the result to a generic rebase failure or infer that no rebase is active. The continue path also demonstrates the unsafe split at src/doltlite_rebase.c:1544-1550 and src/doltlite_rebase.c:1640-1647: bPlanDropped is set only after a successful claim, so a claim-side hard error reaches the pre-claim branch and returns 'rebase failed'. The smallest practical fix is to preserve the original claim error whenever refresh or working-set reload is unsuccessful, return the recovery-failure message, and only report 'no rebase in progress' after a successful durable-state read proves that isRebasing is clear.
  • Why this is likely a bug: The failure is reproduced by a deterministic C regression with an injected claim/refresh fault, and the source contains the exact early-return and error-routing paths that produce the wrong contract. The expected behavior is explicit in the regression oracle: when durable state cannot be read, abort must report recovery failure and must not claim that a peer cleared the rebase. The PR's changed abort claim flow is the direct cause, so a targeted correction to its error decision is appropriate.
Relevant code

src/doltlite_rebase.c:1057-1063

rc = chunkStoreLockAndRefresh(cs);
if( rc!=SQLITE_OK ) return rc;
...
rc = chunkStoreForceRefresh(cs);
if( rc!=SQLITE_OK ) goto claim_done;
rc = doltliteLoadWorkingSet(db, zBranch);
if( rc!=SQLITE_OK ) goto claim_done;

src/doltlite_rebase.c:1404-1447

rc = rebaseClaimActiveEndRetry(db);
...
if( rc!=SQLITE_OK ){
  ...
  if( !stillRebasing ){
    sqlite3_result_error(context, "no rebase in progress", -1);
  }else{
    rebaseResultRecoveryFailure(context, rc);
  }
  return;
}
...
sqlite3_result_text(context, "Interactive rebase aborted", -1, SQLITE_STATIC);

src/doltlite_rebase.c:1544-1550

rc = rebaseClaimActiveEndRetry(db);
if( rc==SQLITE_DONE ){ ... }
if( rc!=SQLITE_OK ) goto abort_err;
bPlanDropped = 1;

test/doltlite_regression_test_c.c:6948-6959

gRegressionFaultCode = 953;
...
res = queryScalarText(db, "SELECT dolt_rebase('--abort')");
...
check("rebase_abort_recovery_failure_is_returned",
      strstr(res, "ERROR: rebase recovery failed")!=0);
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Abort hides refresh failures during recovery**

**What failed:** The recovery regression completed 11 of 12 checks, but rebase_abort_recovery_failure_is_returned failed. The abort operation did not return the required 'ERROR: rebase recovery failed' result.

- **Impact:** When a rebase abort cannot verify durable state, users receive a generic failure instead of a recovery-failure message. They may not know that the rebase state is unverified and needs recovery guidance.
- **Steps to reproduce:**
  1. Create a repository with main and feat branches and start an interactive rebase from feat onto main.
  2. Inject a non-BUSY claim error followed by a force-refresh or durable working-set reload failure.
  3. Run dolt_rebase('--abort').
  4. Check the returned error and the persisted isRebasing state.
- **Stub / mock content:** The test used controlled SQLite fault injection to model a hard claim and refresh failure. No production services, customer data, or application-route mocks were used.
- **Code analysis:** The new claim path in rebaseClaimActiveEnd() calls chunkStoreLockAndRefresh(), chunkStoreForceRefresh(), and doltliteLoadWorkingSet() at src/doltlite_rebase.c:1057-1063. Any non-OK result exits before the durable claim completes. The caller then handles the claim result at src/doltlite_rebase.c:1404-1447, where the PR added a refresh-and-reload check before deciding whether the peer won; if those checks cannot read durable state, the code must retain the original claim error and call rebaseResultRecoveryFailure(), not downgrade the result to a generic rebase failure or infer that no rebase is active. The continue path also demonstrates the unsafe split at src/doltlite_rebase.c:1544-1550 and src/doltlite_rebase.c:1640-1647: bPlanDropped is set only after a successful claim, so a claim-side hard error reaches the pre-claim branch and returns 'rebase failed'. The smallest practical fix is to preserve the original claim error whenever refresh or working-set reload is unsuccessful, return the recovery-failure message, and only report 'no rebase in progress' after a successful durable-state read proves that isRebasing is clear.
- **Why this is likely a bug:** The failure is reproduced by a deterministic C regression with an injected claim/refresh fault, and the source contains the exact early-return and error-routing paths that produce the wrong contract. The expected behavior is explicit in the regression oracle: when durable state cannot be read, abort must report recovery failure and must not claim that a peer cleared the rebase. The PR's changed abort claim flow is the direct cause, so a targeted correction to its error decision is appropriate.

**Relevant code:**

`src/doltlite_rebase.c:1057-1063`

~~~C
rc = chunkStoreLockAndRefresh(cs);
if( rc!=SQLITE_OK ) return rc;
...
rc = chunkStoreForceRefresh(cs);
if( rc!=SQLITE_OK ) goto claim_done;
rc = doltliteLoadWorkingSet(db, zBranch);
if( rc!=SQLITE_OK ) goto claim_done;
~~~

`src/doltlite_rebase.c:1404-1447`

~~~C
rc = rebaseClaimActiveEndRetry(db);
...
if( rc!=SQLITE_OK ){
  ...
  if( !stillRebasing ){
    sqlite3_result_error(context, "no rebase in progress", -1);
  }else{
    rebaseResultRecoveryFailure(context, rc);
  }
  return;
}
...
sqlite3_result_text(context, "Interactive rebase aborted", -1, SQLITE_STATIC);
~~~

`src/doltlite_rebase.c:1544-1550`

~~~C
rc = rebaseClaimActiveEndRetry(db);
if( rc==SQLITE_DONE ){ ... }
if( rc!=SQLITE_OK ) goto abort_err;
bPlanDropped = 1;
~~~

`test/doltlite_regression_test_c.c:6948-6959`

~~~C
gRegressionFaultCode = 953;
...
res = queryScalarText(db, "SELECT dolt_rebase('--abort')");
...
check("rebase_abort_recovery_failure_is_returned",
      strstr(res, "ERROR: rebase recovery failed")!=0);
~~~

Tim added 3 commits August 6, 2026 09:40
Left over after best-effort cleanup stopped using it; fails -Werror
unused-variable on assert-enabled / dead-code gates.
Ito regressions: fault 953 (DROP during claim) was reporting
"no rebase in progress" or silent success because isRebasing was
cleared before DROP failed.

- Drop the plan table before clearing durable isRebasing so a storage
  fault during DROP leaves the rebase flag set and returns recovery
  failed.
- Only report "no rebase in progress" when claim returns SQLITE_DONE
  (successful durable read with isRebasing clear).
- After a successful claim, soft-succeed cleanup on BUSY/LOCKED or a
  missing temp branch (concurrent --continue), but keep hard cleanup
  errors as recovery failed.
Empty commit to restart CI; previous failures were Service Unavailable
while resolving action download info, not product code.
@itoqa

itoqa Bot commented Aug 6, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report9c289df420ac78: 32 test cases ran, 2 new failures ❌, 3 fixed ✅, 26 passing ✅, 1 additional finding ⚠️.

Diff Summary

Coverage spans normal rebase completion and cancellation, reopening and persistence across connections, concurrent continue/abort ownership, branch and repository cleanup, and storage-failure recovery. The broader behavior is generally healthy, but fresh or reopened connections cannot reliably continue a valid rebase, exposing a core workflow failure.

Not safe to merge yet — this PR introduces a high-severity regression in completing valid rebases after a connection change, with a related medium-severity failure for reopened rebases that can discard the expected replayed changes. An unrelated medium-severity missing-session-branch finding is a flag for later, not a driver of this verdict.

Tests run by Ito

View full run

Result State Severity Type Description
❌ New Failure High severity Durable A fresh connection started with a valid interactive rebase plan, but --continue returned an error saying the rebase failed and the branch was restored to its pre-rebase state. The expected finalized refs and post-rebase commit were not produced.
❌ New Failure Medium severity Durable The reopened database showed the saved plan and temporary branch, but continue returned 'rebase failed — branch restored to pre-rebase state'. The temporary state was cleared and a later commit succeeded, yet the expected replayed feature changes were never applied.
❌->✅ Fixed Cleanup The recovery check completed successfully, with all 44 checks passing and no bad recovery outcomes. Genuine storage errors remain distinguishable from a peer that already finished cleanup.
❌->✅ Fixed Contract The fault-injection suite completed all 24 checks successfully. Refresh, restoration, and plan-removal faults were reported with the expected error category instead of being mistaken for a completed rebase.
❌->✅ Fixed Durable A storage failure while ending a rebase returned a recovery error instead of claiming that no rebase was active. The rebase state and plan survived reopening, and a later commit succeeded.
Passing Claim Aborting an active rebase returns a storage error without claiming the rebase or clearing its durable state. The targeted native regression passed all 9 checks.
Passing Claim Claim-related storage failures are reported without clearing the active rebase or leaving the graph lock held. The native fault-injection checks passed all 21 assertions.
Passing Claim The lock contention checks passed. A child process did not keep a released lock, only one contender won promotion, and the related 12-trial race ended with zero bad outcomes and successful follow-up commits.
Passing Claim The available test could not inject lock exhaustion, but source checks show that lock retries are bounded and failed lock refreshes release their resources. A fresh connection is not left blocked by this path.
Passing Claim A working-set reload failure is reported as a recovery failure instead of falsely saying that no rebase is active, and the database lock is released.
Passing Claim Running continue and abort at the same time leaves exactly one action in charge of ending the rebase. The other action exits cleanly, and the repository still accepts a new commit afterward.
Passing Cleanup Deleting the temporary branch before abort cleanup is treated as successful completion. The native concurrency harness completed all 44 checks with zero bad outcomes, and the repository remained usable.
Passing Cleanup Aborting a rebase leaves an independent change on the other branch intact, and both branches remain usable for later work.
Passing Cleanup Reopening after concurrent cleanup did not bring back the deleted temporary branch. The native harness completed all 44 checks with zero bad outcomes, and the repository remained usable.
Passing Cleanup Running continue and abort at the same time leaves one clear winner, avoids a recovery error, and keeps the repository ready for the next commit.
Passing Cleanup Rebase races with garbage collection and branch checkout completed successfully. Concurrent continue and abort also finished without a bad recovery result, and all 44 harness checks passed.
Passing Contract All 12 concurrent rebase trials completed successfully with no recovery errors or failed assertions.
Passing Contract Continuing a valid rebase returned the successful update message, removed the temporary rebase state, and left the branch ready for a new commit.
Passing Contract When continue and abort ran at the same time, one operation finished the rebase and the other stopped without replaying changes. All 12 trials passed, and the repository accepted a later commit.
Passing Contract Abort cleanup keeps a clear recovery error for hard storage failures, while ordinary concurrent completion is treated as a successful abort. The native rebase suite passed 24/24 cases and the concurrent continue-versus-abort harness passed 44/44 checks with no bad outcomes.
Passing Contract Completing or canceling a rebase leaves the saved state consistent after the database is reopened. No stale plan or temporary branch remains, and a new commit can be made.
Passing Contract Aborting the rebase restored the feature branch, kept main as the default branch, and preserved the expected three rows. A new row could then be committed, confirming the repository remained usable.
Passing Contract Running the two terminal actions in either order completed the rebase checks successfully. The second action correctly found no rebase still in progress, and cleanup state remained consistent.
Passing Contract After the concurrent rebase race, the repository stayed usable. The follow-up rebase checks completed all 24 assertions with no failures.
Passing Contract Native recovery checks passed, and the full rebase suite also passed. Abort cleanup returns the expected success message while hard recovery failures remain reported as errors.
Passing Durable The tool rejected the bad plan before replay and kept the active rebase data available for repair.
Passing Durable Concurrent continue and abort handling passed all 56 native checks. The race trials reported zero bad outcomes, so a stale continue did not replay work after the other connection removed the plan.
Passing Durable A replay conflict and an invalid rebase plan both kept the repository state consistent. Retrying after reopening did not replay cleared work, and the repository remained usable.
Passing Durable Reopening an interrupted rebase and continuing without an active rebase kept the saved state, plan, refs, and working data consistent across 33 checks.
Passing Rebase When abort and continue ran at the same time, exactly one operation finished the rebase in every trial. The other operation reported the expected no-rebase result, and the database accepted a later commit.
Passing Rebase The rebase regression suite passed all 24 checks. After one connection finished, stale continue and abort requests did not replay changes or undo the result, and later commits remained usable.
⏸️ Skipped Cleanup Aborting an interactive rebase returned success, left the repository on the expected branch, and removed the rebase plan and temporary branch.
⚠️ Additional Finding Medium severity Claim The abort operation does not reach the cleanup claim when the saved session branch is missing or empty. Instead, it returns a no-rebase-in-progress error, so the expected fallback to main, cleanup of copied branch names, and post-cleanup repository check cannot occur.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Abort rejects a missing session branch
  • Severity: Medium Medium severity
  • Description: The abort operation does not reach the cleanup claim when the saved session branch is missing or empty. Instead, it returns a no-rebase-in-progress error, so the expected fallback to main, cleanup of copied branch names, and post-cleanup repository check cannot occur.
  • Impact: When an active rebase has a missing saved session branch, aborting it can fail and leave the rebase uncleared. The user sees a misleading message and may need to recover the repository manually.
  • Steps to Reproduce:
    1. Start an interactive rebase and leave its durable rebase state active.
    2. Set the persisted session branch to null or an empty value while keeping the rebase active.
    3. Run the rebase abort operation.
    4. Check the result and then inspect the rebase state and branch references after reopening the database.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: src/doltlite_rebase.c:1073-1076 assigns zBranch from doltliteGetSessionBranch(db) and substitutes the literal main only when that value is null or empty. That fallback is the behavior CLAIM-8 is meant to verify. However, src/doltlite_rebase.c:1427-1431 reads the persisted rebase state first and returns the user-facing no-rebase-in-progress error whenever zOrigBranchConst or zReturnBranchConst is null, without calling rebaseClaimActiveEndRetry. Consequently, a null/empty persisted session state can be rejected before the claim function gets a chance to select main. The practical fix is to preserve or reconstruct the required cleanup branch names before this guard, or to allow the claim path to handle the missing session branch while retaining enough copied state for cleanup; do not report no rebase in progress merely because the session branch value is absent while durable isRebasing remains set. The native harness did not create this fixture, so this classification relies on the source-level defect hypothesis and the recorded source evidence.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread src/doltlite_rebase.c Outdated
** Drop the plan table before clearing isRebasing so a fault during DROP leaves
** the durable flag set (fault 953 / recovery-failure regressions). Callers must
** copy branch names out of session state before claiming. */
static int rebaseClaimActiveEnd(sqlite3 *db){

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View replay

🆕 New Failure: identified in this diff run

Medium severity Reopened rebase cannot continue

What failed: The reopened database showed the saved plan and temporary branch, but continue returned 'rebase failed — branch restored to pre-rebase state'. The temporary state was cleared and a later commit succeeded, yet the expected replayed feature changes were never applied.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: Users who reopen an unfinished rebase cannot continue it, so the saved changes are not replayed. The branch is restored, but users must recover the rebase another way instead of completing the expected workflow.
  • Steps to Reproduce:
    1. Create a main branch and a feature branch with committed changes, then start an interactive rebase from the feature branch onto main.
    2. Close the connection that started the rebase before continuing.
    3. Open a fresh connection and confirm that the saved plan has one row and the temporary rebase branch exists.
    4. Run dolt_rebase('--continue').
    5. Inspect the branches and make a normal commit after the command returns.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The relevant implementation is src/doltlite_rebase.c. The PR adds rebaseClaimActiveEnd at lines 1066-1125: it refreshes and reloads the working set, drops main.dolt_rebase at lines 1095-1097, then clears and persists isRebasing at lines 1112-1119. The continue path reads the plan at lines 1553-1561, then the PR-added claim-before-replay block at lines 1603-1611 calls rebaseClaimActiveEndRetry and marks bPlanDropped before replay starts. Replay and branch finalization then depend on the in-memory plan and branch context through rebaseReplayPlanGroup and doltliteMutateRefs at lines 1613-1647. The recorded fresh-connection run proves that durable metadata was readable before continue, but this path still failed and entered abort_err at lines 1701-1735, which reports the restored-branch error after cleanup. This narrows the defect to the changed claim/replay/finalization interaction rather than missing setup: the claim must not invalidate the durable replay context, or continue must reload that context after claiming. A targeted fix should retain a valid plan/context snapshot through the claim or reopen and validate it before replay, while keeping the one-owner behavior.
  • Why this is likely a bug: This is a real product failure because the public dolt_rebase('--continue') operation rejects a valid, persisted rebase after a normal close-and-reopen. The test did not inject faults, mocks, or unsupported commands: it observed plan_rows=1 and temp_ref=1 before the operation, then the application itself returned the recovery error and restored the branch. The PR explicitly changes the ownership and cleanup ordering used by continue, and those changed lines clear the durable state immediately before replay; the failure is therefore directly tied to the PR's new path. The practical fix is targeted: keep the valid durable plan and branch names available after the ownership claim, or reload them before replay and final ref cleanup, then retain the existing post-claim cleanup behavior.
Relevant code

src/doltlite_rebase.c:1066-1119

static int rebaseClaimActiveEnd(sqlite3 *db){ ... sqlite3_exec(db, "DROP TABLE IF EXISTS main.dolt_rebase", ...); ... doltliteClearSessionRebaseState(db); ... doltliteSaveWorkingSet(db);

src/doltlite_rebase.c:1551-1614

rc = doltliteValidateRebasePlanTable(db, &zPlanErr); ... rc = rebaseReadPlan(db, &aPlan, &nPlan); ... rc = rebaseClaimActiveEndRetry(db); ... bPlanDropped = 1; ... rc = doltliteFlushCatalogToHash(db, &curCat);

src/doltlite_rebase.c:1701-1735

abort_err: ... recoveryRc = rebaseDiscardWorkingBranch(...); ... sqlite3_result_error(context, "rebase failed — branch restored to pre-rebase state", -1);
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Reopened rebase cannot continue**

**What failed:** The reopened database showed the saved plan and temporary branch, but continue returned 'rebase failed — branch restored to pre-rebase state'. The temporary state was cleared and a later commit succeeded, yet the expected replayed feature changes were never applied.

- **Impact:** Users who reopen an unfinished rebase cannot continue it, so the saved changes are not replayed. The branch is restored, but users must recover the rebase another way instead of completing the expected workflow.
- **Steps to reproduce:**
  1. Create a main branch and a feature branch with committed changes, then start an interactive rebase from the feature branch onto main.
  2. Close the connection that started the rebase before continuing.
  3. Open a fresh connection and confirm that the saved plan has one row and the temporary rebase branch exists.
  4. Run dolt_rebase('--continue').
  5. Inspect the branches and make a normal commit after the command returns.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The relevant implementation is src/doltlite_rebase.c. The PR adds rebaseClaimActiveEnd at lines 1066-1125: it refreshes and reloads the working set, drops main.dolt_rebase at lines 1095-1097, then clears and persists isRebasing at lines 1112-1119. The continue path reads the plan at lines 1553-1561, then the PR-added claim-before-replay block at lines 1603-1611 calls rebaseClaimActiveEndRetry and marks bPlanDropped before replay starts. Replay and branch finalization then depend on the in-memory plan and branch context through rebaseReplayPlanGroup and doltliteMutateRefs at lines 1613-1647. The recorded fresh-connection run proves that durable metadata was readable before continue, but this path still failed and entered abort_err at lines 1701-1735, which reports the restored-branch error after cleanup. This narrows the defect to the changed claim/replay/finalization interaction rather than missing setup: the claim must not invalidate the durable replay context, or continue must reload that context after claiming. A targeted fix should retain a valid plan/context snapshot through the claim or reopen and validate it before replay, while keeping the one-owner behavior.
- **Why this is likely a bug:** This is a real product failure because the public dolt_rebase('--continue') operation rejects a valid, persisted rebase after a normal close-and-reopen. The test did not inject faults, mocks, or unsupported commands: it observed plan_rows=1 and temp_ref=1 before the operation, then the application itself returned the recovery error and restored the branch. The PR explicitly changes the ownership and cleanup ordering used by continue, and those changed lines clear the durable state immediately before replay; the failure is therefore directly tied to the PR's new path. The practical fix is targeted: keep the valid durable plan and branch names available after the ownership claim, or reload them before replay and final ref cleanup, then retain the existing post-claim cleanup behavior.

**Relevant code:**

`src/doltlite_rebase.c:1066-1119`

~~~c
static int rebaseClaimActiveEnd(sqlite3 *db){ ... sqlite3_exec(db, "DROP TABLE IF EXISTS main.dolt_rebase", ...); ... doltliteClearSessionRebaseState(db); ... doltliteSaveWorkingSet(db);
~~~

`src/doltlite_rebase.c:1551-1614`

~~~c
rc = doltliteValidateRebasePlanTable(db, &zPlanErr); ... rc = rebaseReadPlan(db, &aPlan, &nPlan); ... rc = rebaseClaimActiveEndRetry(db); ... bPlanDropped = 1; ... rc = doltliteFlushCatalogToHash(db, &curCat);
~~~

`src/doltlite_rebase.c:1701-1735`

~~~c
abort_err: ... recoveryRc = rebaseDiscardWorkingBranch(...); ... sqlite3_result_error(context, "rebase failed — branch restored to pre-rebase state", -1);
~~~

Tim and others added 2 commits August 6, 2026 17:15
Claim the canonical interactive-rebase working state before either terminal operation drops the plan or opens its write path. This keeps the loser read-only and prevents stale refs from restoring the temporary branch.

Require exactly one race winner, verify winner-specific data and cleanup, cover reopen/continue on ordinary branch names, and record the concurrency guarantee.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Check the race barrier pipe operations so fortified checked builds do not reject ignored I/O results. Re-read canonical rebase state under the graph lock when continue loses before claiming, mapping a concurrently removed plan to the required no-rebase result.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@itoqa

itoqa Bot commented Aug 7, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report420ac78d6c332d: 25 test cases ran, 1 new failure ❌, 3 fixed ✅, 21 passing ✅.

Diff Summary

The run broadly exercised rebase completion and cancellation, reopening and cleanup, concurrent operations, storage and lock failures, malformed or missing state, and recovery of the repository for subsequent work. These happy-path, edge-case, and adversarial scenarios were largely healthy, but the concurrency validation does not fully enforce completion outcomes.

Merge with caution — the PR introduces a medium-severity gap in concurrent-operation validation that could allow an incomplete child process to be accepted as a successful result, weakening protection against faulty race behavior. The issue does not indicate direct data loss or corruption, but it should be addressed before relying on this test as a merge safeguard.

Tests run by Ito

View full run

Result State Severity Type Description
❌ New Failure Medium severity Contract The checker decides whether a trial passed from the two child output strings, but it does not reject a child that exited with an error or signal. A child that did not complete can therefore be counted as a valid winner or loser.
❌->✅ Fixed Claim Rebase cleanup completed successfully when the saved session branch was missing. The original branch and temporary cleanup state stayed correct, and the repository remained usable afterward.
❌->✅ Fixed Durable Continuing the rebase completed successfully, removed the temporary branch, cleared the active rebase state, and left the repository ready for another commit.
❌->✅ Fixed Durable Continuing the rebase after reopening the connection completed successfully. The correct branch state was restored, the temporary rebase branch was removed, and the repository remained ready for a normal commit.
Passing Claim An abort request with no working branch name is rejected, and the active rebase state remains available for recovery.
Passing Claim Concurrent continue and abort operations produced exactly one winner in every trial, and the repository remained usable afterward.
Passing Claim The rebase operation retries lock contention but returns other storage errors as recovery failures. The local regression passed all 12 checks, and the broader rebase suite passed all 26 checks.
Passing Claim The continue and abort race completed all 45 checks. Exactly one operation won each trial, and the repository stayed usable afterward.
Passing Cleanup Aborting after the repository was reopened restored the original branch and durable state, and all 23 checks passed.
Passing Cleanup The cleanup process reported the first recovery error and did not claim that the branch was restored or that the other operation finished.
Passing Cleanup Continue and abort both handled injected finalization failures without claiming success or leaving stale terminal state.
Passing Cleanup Aborting the rebase did not overwrite the other connection's commit. The peer row stayed in the branch, the peer commit remained the branch head, and the repository stayed usable after reopening.
Passing Contract All 12 continue-versus-abort race trials passed with no recovery errors, lost output, or bad results. The repository also accepted a follow-up commit after each race.
Passing Contract Rebase read errors return an error instead of looking like a successful peer completion. Reopening the database also keeps the rebase state and durable data intact.
Passing Contract The contract checks passed, and all synchronized continue-versus-abort races had exactly one winner with valid loser results.
Passing Contract Contention tests completed all 45 trials successfully. Rebase operations handled busy and locked storage states without duplicate terminal work or stale state.
Passing Contract Injected storage failures during rebase continue and abort return a clear recovery error instead of claiming success or saying that another operation finished.
Passing Contract Abort and continue each finish the rebase only once. The second terminal action reports that no rebase is in progress, and all 26 contract checks pass.
Passing Contract A commit made by another connection stayed intact when the rebase could not continue. The repository also passed the full C regression suite with no failures.
Passing Contract Injected recovery and upstream-history faults returned the expected errors without being reported as a successful peer finish.
Passing Durable Continue and abort raced across twelve trials, and every trial left the repository with one clean result, no leftover rebase state, and a successful follow-up commit.
Passing Durable The active rebase state stayed available while another process held a lock, and the synchronized fixture completed successfully.
Passing Durable Reopening the short feature branch and continuing the rebase completed successfully. The expected rows remained available and the temporary rebase branch was removed.
Passing Rebase Concurrent abort and continue operations produced exactly one winner in every trial. The losing operation reported that no rebase was in progress, cleanup completed, and a new commit succeeded afterward.
Passing Rebase A second request could not replay or undo a rebase after another connection finished it. The repository stayed usable for a later commit.
⏸️ Skipped Claim Aborting an active rebase returns a storage error without claiming the rebase or clearing its durable state. The targeted native regression passed all 9 checks.
⏸️ Skipped Claim Claim-related storage failures are reported without clearing the active rebase or leaving the graph lock held. The native fault-injection checks passed all 21 assertions.
⏸️ Skipped Claim The lock contention checks passed. A child process did not keep a released lock, only one contender won promotion, and the related 12-trial race ended with zero bad outcomes and successful follow-up commits.
⏸️ Skipped Claim The available test could not inject lock exhaustion, but source checks show that lock retries are bounded and failed lock refreshes release their resources. A fresh connection is not left blocked by this path.
⏸️ Skipped Claim A working-set reload failure is reported as a recovery failure instead of falsely saying that no rebase is active, and the database lock is released.
⏸️ Skipped Claim Running continue and abort at the same time leaves exactly one action in charge of ending the rebase. The other action exits cleanly, and the repository still accepts a new commit afterward.
⏸️ Skipped Cleanup Reopening after concurrent cleanup did not bring back the deleted temporary branch. The native harness completed all 44 checks with zero bad outcomes, and the repository remained usable.
⏸️ Skipped Cleanup The recovery check completed successfully, with all 44 checks passing and no bad recovery outcomes. Genuine storage errors remain distinguishable from a peer that already finished cleanup.
⏸️ Skipped Cleanup Running continue and abort at the same time leaves one clear winner, avoids a recovery error, and keeps the repository ready for the next commit.
⏸️ Skipped Cleanup Rebase races with garbage collection and branch checkout completed successfully. Concurrent continue and abort also finished without a bad recovery result, and all 44 harness checks passed.
⏸️ Skipped Contract Continuing a valid rebase returned the successful update message, removed the temporary rebase state, and left the branch ready for a new commit.
⏸️ Skipped Contract When continue and abort ran at the same time, one operation finished the rebase and the other stopped without replaying changes. All 12 trials passed, and the repository accepted a later commit.
⏸️ Skipped Contract Abort cleanup keeps a clear recovery error for hard storage failures, while ordinary concurrent completion is treated as a successful abort. The native rebase suite passed 24/24 cases and the concurrent continue-versus-abort harness passed 44/44 checks with no bad outcomes.
⏸️ Skipped Contract Completing or canceling a rebase leaves the saved state consistent after the database is reopened. No stale plan or temporary branch remains, and a new commit can be made.
⏸️ Skipped Contract Aborting the rebase restored the feature branch, kept main as the default branch, and preserved the expected three rows. A new row could then be committed, confirming the repository remained usable.
⏸️ Skipped Contract Native recovery checks passed, and the full rebase suite also passed. Abort cleanup returns the expected success message while hard recovery failures remain reported as errors.
⏸️ Skipped Durable The tool rejected the bad plan before replay and kept the active rebase data available for repair.
⏸️ Skipped Durable A storage failure while ending a rebase returned a recovery error instead of claiming that no rebase was active. The rebase state and plan survived reopening, and a later commit succeeded.
⏸️ Skipped Durable Concurrent continue and abort handling passed all 56 native checks. The race trials reported zero bad outcomes, so a stale continue did not replay work after the other connection removed the plan.
⏸️ Skipped Durable A replay conflict and an invalid rebase plan both kept the repository state consistent. Retrying after reopening did not replay cleared work, and the repository remained usable.
⏸️ Skipped Durable Reopening an interrupted rebase and continuing without an active rebase kept the saved state, plan, refs, and working data consistent across 33 checks.

Tip

Reply with @itoqa to send us feedback on this test run.

abortOut[n] = 0;
close(pipefd[0]);
}
waitpid(child, &status, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

🆕 New Failure: identified in this diff run

Medium severity Race checker accepts incomplete child runs

What failed: The checker decides whether a trial passed from the two child output strings, but it does not reject a child that exited with an error or signal. A child that did not complete can therefore be counted as a valid winner or loser.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: A faulty concurrent rebase result could reach users without this test catching it. There is no evidence that this defect directly causes user data loss or corruption.
  • Steps to Reproduce:
    1. Build and run the multiprocess rebase test.
    2. Arrange for the abort child to print a winner-like result and then exit with a nonzero status or because of a signal.
    3. Let the parent collect both output strings and finish the trial.
    4. Observe that the trial can still be accepted when the output strings contain exactly one winner and one loser message.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The recorded normal run completed all 45 assertions with 12 concurrent continue-versus-abort trials, wins_continue=4, wins_abort=8, and bad=0. The separate interrupted run ended with exit 124 before child-output collection, so it is not proof of an application rebase failure; the defect is established by source inspection. In the PR-added test/multi_process_merge_rebase_test.c, the parent calls waitpid(child, &status, 0) at lines 606-606, but does not inspect status afterward. Lines 608-611 derive contWin, abortWin, contLost, and abortLost only by searching contOut and abortOut. Lines 612-613 then accept the trial when exactlyOneWin and loserOk are true, with no WIFEXITED or WEXITSTATUS condition. Therefore a child can write a valid-looking response and then terminate abnormally, while the parent still accepts the trial. This directly violates CONTRACT-20's incomplete-child oracle requirement. The targeted remediation is to validate the wait status immediately after waitpid and include successful normal child exit in the acceptance predicate, treating signaled, nonzero, or failed wait results as bad trials.
  • Why this is likely a bug: This is not caused by a mock or setup failure: the PR changes the actual test oracle, and the missing status check is visible in the code path that decides whether a trial is valid. The test's stated contract requires incomplete child events to fail, yet the acceptance condition can be satisfied using output emitted before abnormal termination. A focused wait-status check is sufficient; no broad production-code change is needed.
Relevant code

test/multi_process_merge_rebase_test.c:606-613

    waitpid(child, &status, 0);

    contWin = strstr(contOut, "Successfully")!=0;
    abortWin = strstr(abortOut, "Interactive rebase aborted")!=0;
    contLost = strstr(contOut, "no rebase in progress")!=0;
    abortLost = strstr(abortOut, "no rebase in progress")!=0;
    exactlyOneWin = contWin ^ abortWin;
    loserOk = (contWin && abortLost) || (abortWin && contLost);

test/multi_process_merge_rebase_test.c:617-621

    if( !exactlyOneWin || !loserOk ){
      bad++;
      fprintf(stderr, "FAIL trial %d cont=[%s] abort=[%s]\n",
              trial, contOut, abortOut);
    }
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Race checker accepts incomplete child runs**

**What failed:** The checker decides whether a trial passed from the two child output strings, but it does not reject a child that exited with an error or signal. A child that did not complete can therefore be counted as a valid winner or loser.

- **Impact:** A faulty concurrent rebase result could reach users without this test catching it. There is no evidence that this defect directly causes user data loss or corruption.
- **Steps to reproduce:**
  1. Build and run the multiprocess rebase test.
  2. Arrange for the abort child to print a winner-like result and then exit with a nonzero status or because of a signal.
  3. Let the parent collect both output strings and finish the trial.
  4. Observe that the trial can still be accepted when the output strings contain exactly one winner and one loser message.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The recorded normal run completed all 45 assertions with 12 concurrent continue-versus-abort trials, wins_continue=4, wins_abort=8, and bad=0. The separate interrupted run ended with exit 124 before child-output collection, so it is not proof of an application rebase failure; the defect is established by source inspection. In the PR-added test/multi_process_merge_rebase_test.c, the parent calls waitpid(child, &status, 0) at lines 606-606, but does not inspect status afterward. Lines 608-611 derive contWin, abortWin, contLost, and abortLost only by searching contOut and abortOut. Lines 612-613 then accept the trial when exactlyOneWin and loserOk are true, with no WIFEXITED or WEXITSTATUS condition. Therefore a child can write a valid-looking response and then terminate abnormally, while the parent still accepts the trial. This directly violates CONTRACT-20's incomplete-child oracle requirement. The targeted remediation is to validate the wait status immediately after waitpid and include successful normal child exit in the acceptance predicate, treating signaled, nonzero, or failed wait results as bad trials.
- **Why this is likely a bug:** This is not caused by a mock or setup failure: the PR changes the actual test oracle, and the missing status check is visible in the code path that decides whether a trial is valid. The test's stated contract requires incomplete child events to fail, yet the acceptance condition can be satisfied using output emitted before abnormal termination. A focused wait-status check is sufficient; no broad production-code change is needed.

**Relevant code:**

`test/multi_process_merge_rebase_test.c:606-613`

~~~c
    waitpid(child, &status, 0);

    contWin = strstr(contOut, "Successfully")!=0;
    abortWin = strstr(abortOut, "Interactive rebase aborted")!=0;
    contLost = strstr(contOut, "no rebase in progress")!=0;
    abortLost = strstr(abortOut, "no rebase in progress")!=0;
    exactlyOneWin = contWin ^ abortWin;
    loserOk = (contWin && abortLost) || (abortWin && contLost);
~~~

`test/multi_process_merge_rebase_test.c:617-621`

~~~c
    if( !exactlyOneWin || !loserOk ){
      bad++;
      fprintf(stderr, "FAIL trial %d cont=[%s] abort=[%s]\n",
              trial, contOut, abortOut);
    }
~~~

@timsehn
timsehn merged commit af30c51 into master Aug 7, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant