--- /dev/null
+#!/bin/bash
+# Test 21: an EOF-tail extent is rewritten only when it frees a WHOLE block
+#
+# Regression guard for the phantom-unreachable-block bug (cf3959bc4) and a
+# runtime exercise of the past-EOF temp elision (f04f91a14). Runs at the
+# DEFAULT free-min = 50%.
+#
+# MECHANISM
+# btrfs does not partially free a data extent: truncating a file within an
+# extent leaves the whole physical extent allocated and only lowers
+# i_size, so whole blocks past EOF become allocated-but-unreachable. bees
+# reclaims them by COPYING the reachable data to a compact extent (a
+# cleanup plan -- no dedupe, viable with no candidates). The gain is the
+# number of COMPLETE unreachable blocks; a sub-block past-EOF tail shares
+# its block with reachable data and frees nothing. That count is the
+# floor() in scan_next_region_block_count.
+#
+# For an extent of E blocks with reachable data D blocks (= bad, copied)
+# and U complete unreachable blocks (= good, reclaimed), U = E - D, and
+# free-min at 50% passes iff good >= bad, i.e. U >= D, i.e. retained data
+# is at most half the extent.
+#
+# THE BUG: the unreachable region was rounded UP, so a sub-block-only tail
+# counted as 1 phantom freeable block. On a SINGLE-block extent that
+# phantom is decisive: good_pre = 1 = bad clears free-min at exactly 50%
+# and the negative debt rewrites the extent for zero net gain (pure ref
+# churn). Post-fix the tail rounds DOWN to 0 -> free-min rejects and
+# do-nothing wins. (Only E=1 flips at 50%; for E>=2 the phantom never
+# cleared free-min, so the multi-block cases here test that REAL gains
+# still fire and the executor handles the past-EOF tail, not the flip.)
+#
+# WHY A COPY, NOT A DEDUPE: each file is independent random data, so there
+# is no duplicate to reference -- the only plan is the cleanup copy. A
+# dedupe would correctly have negative debt and is not what regressed.
+#
+# CASES (all at default free-min = 50%):
+# A 4096 -> 2990 E=1 D=1 U=0 NOT rewritten (post) / rewritten (pre) <- guard
+# B 8192 -> 2990 E=2 D=1 U=1 rewritten (1-block gain, floor of mixed block)
+# C 16384 -> 6000 E=4 D=2 U=2 rewritten (2-block gain, floor of mixed block)
+#
+# Prerequisites: testlib.sh, a configured host.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
+source "$SCRIPT_DIR/../lib/testlib.sh"
+
+IT_TEST_NAME="21-eof-tail-no-churn"
+
+it_load_config
+it_setup_trap
+
+BEESHOME="$TEST_MOUNT/.beeshome"
+IT_BEES_LOGFILE="$TEST_LOG_DIR/${IT_RUN_ID}.log"
+
+echo "=== $IT_TEST_NAME ==="
+it_deploy
+
+it_ssh_script <<REMOTE
+set -euo pipefail
+
+disk_bytenr() {
+ btrfs-search-metadata file "\$1" 2>/dev/null | \
+ awk '/disk_bytenr/ {for (i=1;i<=NF;i++) if (\$i=="disk_bytenr") {print \$(i+1); exit}}'
+}
+one_extent() { [ "\$(filefrag "\$1" | grep -oE '[0-9]+ extent' | grep -oE '^[0-9]+')" = 1 ]; }
+# Physical allocation length = the EXTENT_ITEM key's offset field (the
+# extent's item-header offset), read from the extent tree. Independent of
+# any file's i_size / reference length. dump-tree prints it as
+# item N key (<bytenr> EXTENT_ITEM <length>) ...
+extent_len() { # bytenr
+ btrfs inspect-internal dump-tree -t extent '${TEST_SCRATCH_DEV}' 2>/dev/null | \
+ grep -F "(\$1 EXTENT_ITEM " | head -1 | \
+ sed -E 's/.*EXTENT_ITEM ([0-9]+)\).*/\1/'
+}
+
+# ── Fresh fs ─────────────────────────────────────────────────────────────────
+umount -l '${TEST_MOUNT}' 2>/dev/null || true
+mkfs.btrfs -f '${TEST_SCRATCH_DEV}'
+mkdir -p '${TEST_MOUNT}'
+mount -o subvolid=5 '${TEST_SCRATCH_DEV}' '${TEST_MOUNT}'
+cd '${TEST_MOUNT}'
+
+# One file per case, DISTINCT random content (no cross-file dedupe), each a
+# single extent. write -> commit -> truncate -> commit.
+make_case() { # name write_bytes truncate_to
+ head -c "\$2" /dev/urandom > "\$1"
+ btrfs filesystem sync '${TEST_MOUNT}'
+ one_extent "\$1" || { echo "[FAIL] \$1 write fragmented; premise broken" >&2; exit 1; }
+ truncate -s "\$3" "\$1"
+ btrfs filesystem sync '${TEST_MOUNT}'
+ # PREMISE: btrfs never partially frees a data extent, so the physical
+ # allocation (EXTENT_ITEM key length) stays at the written size even
+ # though i_size shrank -- that leftover is the unreachable space bees
+ # reclaims. This is thoroughly baked into current btrfs on-disk design;
+ # only a format change (e.g. extent_tree_v2) could relax it, and that
+ # would require significant bees planner work throughout. So a failure
+ # HERE is a canary for a changed btrfs, not a flaky test -- assert it at
+ # setup rather than let cases B/C silently stop reclaiming.
+ local bn plen
+ bn="\$(disk_bytenr "\$1")"
+ plen="\$(extent_len "\$bn")"
+ [ "\$plen" = "\$2" ] || { echo "[FAIL] \$1 extent shrank on truncate: EXTENT_ITEM len \$plen != written \$2 -- btrfs partial-free premise broken" >&2; exit 1; }
+ echo "[premise] \$1: i_size \$3, physical extent still \$plen bytes"
+}
+make_case a_regression 4096 2990 # E=1 D=1 U=0 -> 0 complete unreachable blocks
+make_case b_one_block 8192 2990 # E=2 D=1 U=1 -> 1 complete unreachable block
+make_case c_two_block 16384 6000 # E=4 D=2 U=2 -> 2 complete unreachable blocks
+
+echo "A_PRE=\$(disk_bytenr a_regression)" > /tmp/pre.env
+echo "B_PRE=\$(disk_bytenr b_one_block)" >> /tmp/pre.env
+echo "C_PRE=\$(disk_bytenr c_two_block)" >> /tmp/pre.env
+cat /tmp/pre.env
+
+# ── bees config: DEFAULT free-min (50%), one deterministic pass ──────────────
+mkdir -p '${BEESHOME}' '${TEST_LOG_DIR}'
+cat > '${BEESHOME}/bees.conf' <<'CONF'
+[log.plan]
+level = 8
+[loop]
+exit-one-pass = yes
+[thread]
+thread-max = 1
+[scan.extent.*]
+method = 1
+filter = ACCEPT
+[rewrite]
+free-min = 50%
+CONF
+# free-min is pinned to 50% (the schema default and the production regime
+# in which the regression occurred) so the test is hermetic: the host may
+# carry a global /etc/bees rewrite.free-min (e.g. 128K) that bees merges
+# under this per-run config, and at 128K the 'cap threshold at extent size'
+# clause would require freeing 100% of any sub-128K extent -- masking both
+# the bug and the real-gain cases. At 50%, B/C (free exactly half) pass
+# and A (frees nothing) is declined.
+
+'${REMOTE_BEES_BIN}/bees' --config '${BEESHOME}/bees.conf' '${TEST_MOUNT}' \
+ > '${IT_BEES_LOGFILE}' 2>&1 &
+wait \$! || true
+
+btrfs filesystem sync '${TEST_MOUNT}'
+{
+ echo "A_POST=\$(disk_bytenr a_regression)"
+ echo "B_POST=\$(disk_bytenr b_one_block)"
+ echo "C_POST=\$(disk_bytenr c_two_block)"
+} > '${TEST_LOG_DIR}/${IT_RUN_ID}.results'
+cat '${TEST_LOG_DIR}/${IT_RUN_ID}.results'
+REMOTE
+
+# ── Assertions ───────────────────────────────────────────────────────────────
+eval "$(it_ssh "cat /tmp/pre.env; cat '${TEST_LOG_DIR}/${IT_RUN_ID}.results'")"
+
+it_assert_no_errors "run: no exceptions / Plan incomplete / crash"
+it_assert_bees_log_grep "run: planner evaluated extents" "scan_next_log_plan start bytenr"
+
+# THE FIX: a 0-whole-block EOF tail is declined even at 50% free-min.
+it_assert "A: 0-block EOF tail NOT rewritten (bytenr stable)" \
+ test "$A_PRE" = "$A_POST"
+
+# Real gains still fire at 50% free-min -- proves bees was active (so A's
+# stability is a genuine decline) and drives the executor's bare-past-EOF
+# tail path (f04f91a14); it_assert_no_errors catches any Plan-incomplete.
+it_assert "B: 1-block gain rewritten" test "$B_PRE" != "$B_POST"
+it_assert "C: 2-block gain rewritten" test "$C_PRE" != "$C_POST"
+
+# Strongest form (optional): re-deploy cf3959bc4^ and assert A IS rewritten
+# there -- proving the fixture bites pre-fix. A is the only case that flips
+# at 50% free-min (see header), so it is the sole differential signal.
+
+it_summary