|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Fusebase Flow — v3.25.1 adoption-hop regression: the baseline merge-preserve MUST |
| 3 | +# run on the FIRST upgrade that adopts v3.25.x, even when the consumer's local |
| 4 | +# hooks/local/lib/ was absent when the engine started (a pre-v3.25 install, or a |
| 5 | +# bootstrap that didn't stage lib/). The W2 fix (U3) shipped in v3.25.0 but silently |
| 6 | +# no-op'd on exactly this hop because upgrade.sh sourced the merge lib only from the |
| 7 | +# LOCAL tree — undefined on a pre-v3.25 install -> Step 1a guard false -> merge |
| 8 | +# skipped -> project rows clobbered. |
| 9 | +# |
| 10 | +# RED-then-GREEN, in one run, against the REAL fixed upgrade.sh source-load logic: |
| 11 | +# RED — PRE-FIX source logic (local-only; lib absent) leaves the merge function |
| 12 | +# UNDEFINED, so the project row is LOST after the wholesale policies/ copy. |
| 13 | +# GREEN — POST-FIX source logic (source_merge_lib: $SOURCE_CLONE first) DEFINES the |
| 14 | +# function, the merge runs, the project row SURVIVES, and |
| 15 | +# check-module-size.sh --all passes. |
| 16 | +# The RED arm proves this test genuinely detects the bug (no false-green); the GREEN |
| 17 | +# arm proves P2 fixes it. P1 (bootstrap staging hooks/local/lib/) is asserted |
| 18 | +# separately below by running the real bootstrap copy step against a source tree. |
| 19 | +# |
| 20 | +# Output contract (parsed by run-tests.sh): "PASS: bootstrap-baseline-hop <name>" / |
| 21 | +# "FAIL: bootstrap-baseline-hop <name>"; exit code = number of failures. |
| 22 | + |
| 23 | +set -uo pipefail |
| 24 | + |
| 25 | +ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" |
| 26 | +UPGRADE_SH="$ROOT/hooks/local/upgrade.sh" |
| 27 | +BOOTSTRAP_SH="$ROOT/hooks/local/bootstrap-upgrade.sh" |
| 28 | +SRC_LIB="$ROOT/hooks/local/lib/merge-module-size-baseline.sh" |
| 29 | +python_bin="${PYTHON:-python3}"; command -v "$python_bin" >/dev/null 2>&1 || python_bin="python" |
| 30 | + |
| 31 | +pass=0; fail=0 |
| 32 | +ok() { pass=$((pass + 1)); echo "PASS: bootstrap-baseline-hop $1"; } |
| 33 | +bad() { fail=$((fail + 1)); echo "FAIL: bootstrap-baseline-hop $1 ($2)"; } |
| 34 | +finish() { echo "[test-bootstrap-baseline-hop] $pass/$((pass + fail)) PASS"; exit $fail; } |
| 35 | + |
| 36 | +# Loud setup preconditions — a missing input must FAIL the test, never false-green. |
| 37 | +[ -f "$UPGRADE_SH" ] || { bad "setup-upgrade-present" "missing $UPGRADE_SH"; finish; } |
| 38 | +[ -f "$BOOTSTRAP_SH" ] || { bad "setup-bootstrap-present" "missing $BOOTSTRAP_SH"; finish; } |
| 39 | +[ -f "$SRC_LIB" ] || { bad "setup-srclib-present" "missing $SRC_LIB"; finish; } |
| 40 | +ok "setup-inputs-present" |
| 41 | + |
| 42 | +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT |
| 43 | + |
| 44 | +############################################################################### |
| 45 | +# Extract the REAL source-load logic from the fixed upgrade.sh. |
| 46 | +# We source the function definition out of the actual engine so this test exercises |
| 47 | +# the shipped code, not a paraphrase. The pre-fix behavior is reconstructed |
| 48 | +# faithfully (source ONLY $ROOT/hooks/local/lib, the v3.25.0 logic). |
| 49 | +############################################################################### |
| 50 | +# Pull source_merge_lib() from the live upgrade.sh (awk: from the function header |
| 51 | +# to its closing brace). Portable — avoids a nested python fork. |
| 52 | +ENGINE_FUNCS="$TMP/engine-funcs.sh" |
| 53 | +awk '/^source_merge_lib\(\) \{/{f=1} f{print} f&&/^\}/{exit}' "$UPGRADE_SH" > "$ENGINE_FUNCS" |
| 54 | +# Must capture a complete function (header + closing brace), else P2 is missing/renamed. |
| 55 | +if [ ! -s "$ENGINE_FUNCS" ] || ! head -1 "$ENGINE_FUNCS" | grep -q '^source_merge_lib() {' \ |
| 56 | + || ! tail -1 "$ENGINE_FUNCS" | grep -q '^}'; then |
| 57 | + bad "extract-source-merge-lib" "source_merge_lib not found/complete in upgrade.sh (P2 missing?)"; finish |
| 58 | +fi |
| 59 | +# It must source from $SOURCE_CLONE (the authoritative-tree fix), not local-only. |
| 60 | +grep -q 'SOURCE_CLONE/hooks/local/lib/merge-module-size-baseline.sh' "$ENGINE_FUNCS" \ |
| 61 | + || { bad "extract-source-merge-lib" "source_merge_lib does not source from \$SOURCE_CLONE (P2 incomplete)"; finish; } |
| 62 | +ok "extract-source-merge-lib" |
| 63 | + |
| 64 | +############################################################################### |
| 65 | +# Build a consumer tree that reproduces the adoption hop: |
| 66 | +# - a PROJECT over-ceiling source file frozen in the consumer's baseline; |
| 67 | +# - the project row is ABSENT from the (upstream) baseline being installed; |
| 68 | +# - hooks/local/lib/ is ABSENT locally at engine start (pre-v3.25 install); |
| 69 | +# - $SOURCE_CLONE = the current repo (v3.25.x target, which HAS hooks/local/lib/). |
| 70 | +############################################################################### |
| 71 | +REPO="$TMP/repo" |
| 72 | +GIT=(git -C "$REPO" -c user.name=flow-test -c user.email=flow-test@local) |
| 73 | +mkdir -p "$REPO/policies" "$REPO/hooks/shared" "$REPO/hooks/local" |
| 74 | +git init -q "$REPO" |
| 75 | +cp "$ROOT/policies/module-size.yml" "$REPO/policies/" |
| 76 | +cp "$ROOT/hooks/shared/module_size.py" "$REPO/hooks/shared/" |
| 77 | +cp "$ROOT/hooks/local/check-module-size.sh" "$REPO/hooks/local/" |
| 78 | + |
| 79 | +# Consumer's OWN over-ceiling file (built via shell redirection, not python open()). |
| 80 | +BIG_LINES=900 |
| 81 | +mkdir -p "$REPO/src" || { bad "setup-mkdir-src" "could not create src/"; finish; } |
| 82 | +for _ in $(seq 1 "$BIG_LINES"); do printf 'x = 1\n'; done > "$REPO/src/big.py" |
| 83 | +[ -f "$REPO/src/big.py" ] || { bad "setup-fixture-exists" "src/big.py not created (would false-green)"; finish; } |
| 84 | +ok "setup-fixture-exists" |
| 85 | +actual_lines="$(wc -l < "$REPO/src/big.py" | tr -d ' ')" |
| 86 | +[ "$actual_lines" = "$BIG_LINES" ] || { bad "setup-fixture-line-count" "src/big.py $actual_lines lines, expected $BIG_LINES"; finish; } |
| 87 | +ok "setup-fixture-line-count" |
| 88 | + |
| 89 | +# Consumer's pre-upgrade baseline carries the PROJECT row (the W2 state). |
| 90 | +cat > "$REPO/policies/module-size-baseline.txt" <<EOF |
| 91 | +# FR-25 module-size baseline — over-ceiling files frozen at current size. |
| 92 | +$BIG_LINES src/big.py |
| 93 | +EOF |
| 94 | +"${GIT[@]}" add -A >/dev/null 2>&1; "${GIT[@]}" commit -q -m init >/dev/null 2>&1 |
| 95 | +"${GIT[@]}" ls-files --error-unmatch src/big.py >/dev/null 2>&1 \ |
| 96 | + || { bad "setup-fixture-tracked" "src/big.py not tracked (--all would skip it)"; finish; } |
| 97 | +ok "setup-fixture-tracked" |
| 98 | + |
| 99 | +# Pre-condition: the consumer baseline passes BEFORE the hop. |
| 100 | +( cd "$REPO" && bash hooks/local/check-module-size.sh --all >/dev/null 2>&1 ) \ |
| 101 | + && ok "pre-hop-check-passes" || { bad "pre-hop-check-passes" "check failed before the hop"; finish; } |
| 102 | + |
| 103 | +# Confirm the pre-v3.25 precondition: the consumer has NO local merge lib at start. |
| 104 | +[ ! -e "$REPO/hooks/local/lib/merge-module-size-baseline.sh" ] \ |
| 105 | + && ok "precondition-no-local-lib" || { bad "precondition-no-local-lib" "local lib unexpectedly present"; finish; } |
| 106 | + |
| 107 | +# $SOURCE_CLONE staged as the v3.25.x target (the current repo's lib is authoritative). |
| 108 | +SOURCE_CLONE="$REPO/.fusebase-flow-source" |
| 109 | +mkdir -p "$SOURCE_CLONE/hooks/local/lib" |
| 110 | +cp "$SRC_LIB" "$SOURCE_CLONE/hooks/local/lib/" |
| 111 | +cp "$REPO/policies/module-size-baseline.txt" "$TMP/local-snapshot.txt" # pre-clobber snapshot |
| 112 | + |
| 113 | +# Upstream baseline (installed by the wholesale copy) knows nothing about src/big.py. |
| 114 | +mkdir -p "$SOURCE_CLONE/policies" |
| 115 | +printf '# FR-25 module-size baseline — over-ceiling files frozen at current size.\n' \ |
| 116 | + > "$SOURCE_CLONE/policies/module-size-baseline.txt" |
| 117 | + |
| 118 | +############################################################################### |
| 119 | +# Helper: run ONE arm of the hop in a subshell with a chosen source-load strategy, |
| 120 | +# emulating upgrade.sh Step 1 (clobber policies/ baseline) + Step 1a (guarded merge). |
| 121 | +# Returns 0 if the project row survives in $REPO/policies/module-size-baseline.txt. |
| 122 | +############################################################################### |
| 123 | +run_hop_arm() { |
| 124 | + local strategy="$1" # "prefix" (PRE-FIX: local-only) | "engine" (POST-FIX: source_merge_lib) |
| 125 | + # Restore the consumer's pre-hop baseline, then CLOBBER it as Step 1 does. |
| 126 | + cp "$TMP/local-snapshot.txt" "$REPO/policies/module-size-baseline.txt" |
| 127 | + local snap="$TMP/arm-snap.txt"; cp "$REPO/policies/module-size-baseline.txt" "$snap" |
| 128 | + cp "$SOURCE_CLONE/policies/module-size-baseline.txt" "$REPO/policies/module-size-baseline.txt" # wholesale clobber |
| 129 | + ( |
| 130 | + set +e |
| 131 | + ROOT="$REPO"; SOURCE_CLONE="$SOURCE_CLONE" |
| 132 | + MERGE_LIB="$ROOT/hooks/local/lib/merge-module-size-baseline.sh" |
| 133 | + if [ "$strategy" = "prefix" ]; then |
| 134 | + # PRE-FIX (v3.25.0) source logic: local tree ONLY. lib is absent -> no-op. |
| 135 | + [ -f "$MERGE_LIB" ] && . "$MERGE_LIB" 2>/dev/null |
| 136 | + else |
| 137 | + # POST-FIX: the REAL source_merge_lib() lifted from the shipped upgrade.sh. |
| 138 | + . "$ENGINE_FUNCS" |
| 139 | + source_merge_lib || true |
| 140 | + fi |
| 141 | + # Step 1a guard, verbatim semantics from upgrade.sh. |
| 142 | + if command -v merge_module_size_baseline >/dev/null 2>&1; then |
| 143 | + merge_module_size_baseline "$snap" "$SOURCE_CLONE/policies/module-size-baseline.txt" \ |
| 144 | + "$REPO/policies/module-size-baseline.txt.new" 2>/dev/null |
| 145 | + mv "$REPO/policies/module-size-baseline.txt.new" "$REPO/policies/module-size-baseline.txt" |
| 146 | + fi |
| 147 | + ) |
| 148 | + grep -qxF "$BIG_LINES src/big.py" "$REPO/policies/module-size-baseline.txt" |
| 149 | +} |
| 150 | + |
| 151 | +############################################################################### |
| 152 | +# RED arm — pre-fix source logic must LOSE the project row (proves the test bites). |
| 153 | +############################################################################### |
| 154 | +if run_hop_arm "prefix"; then |
| 155 | + bad "red-prefix-loses-row" "PRE-FIX logic preserved the row — test cannot detect the bug (false-green risk)" |
| 156 | +else |
| 157 | + ok "red-prefix-loses-row" |
| 158 | +fi |
| 159 | + |
| 160 | +############################################################################### |
| 161 | +# GREEN arm — post-fix source logic (the shipped source_merge_lib) PRESERVES the |
| 162 | +# row AND check-module-size --all passes. This is the actual fix. |
| 163 | +############################################################################### |
| 164 | +if run_hop_arm "engine"; then |
| 165 | + ok "green-engine-preserves-row" |
| 166 | +else |
| 167 | + bad "green-engine-preserves-row" "POST-FIX source_merge_lib failed to preserve the project row (P2 broken)" |
| 168 | +fi |
| 169 | +"${GIT[@]}" add -A >/dev/null 2>&1 |
| 170 | +( cd "$REPO" && bash hooks/local/check-module-size.sh --all >/dev/null 2>&1 ) \ |
| 171 | + && ok "green-post-hop-check-passes" \ |
| 172 | + || bad "green-post-hop-check-passes" "check-module-size --all failed after the fixed hop (the W2 bug recurs)" |
| 173 | + |
| 174 | +############################################################################### |
| 175 | +# P1 proof — the REAL bootstrap-upgrade.sh staging step copies hooks/local/lib/ from |
| 176 | +# the source into the consumer BEFORE handoff. Run just the staging logic against a |
| 177 | +# source tree and assert the lib lands locally (the precondition P2 also relies on). |
| 178 | +############################################################################### |
| 179 | +BREPO="$TMP/brepo" |
| 180 | +mkdir -p "$BREPO/hooks/local" "$BREPO/.fusebase-flow-source/hooks/local/lib" |
| 181 | +git init -q "$BREPO" |
| 182 | +cp "$SRC_LIB" "$BREPO/.fusebase-flow-source/hooks/local/lib/" |
| 183 | +# Minimal source tree the bootstrap staging needs (it copies engine scripts + lib/). |
| 184 | +cp "$BOOTSTRAP_SH" "$BREPO/.fusebase-flow-source/hooks/local/" 2>/dev/null || true |
| 185 | +[ ! -e "$BREPO/hooks/local/lib/merge-module-size-baseline.sh" ] \ |
| 186 | + && ok "p1-precondition-no-lib" || bad "p1-precondition-no-lib" "lib present before staging" |
| 187 | +# Exercise the staging block directly (the part the handoff added). |
| 188 | +( |
| 189 | + set -e |
| 190 | + cd "$BREPO" |
| 191 | + SOURCE_CLONE=".fusebase-flow-source"; TS="test" |
| 192 | + if [ -d "$SOURCE_CLONE/hooks/local/lib" ]; then |
| 193 | + [ -d hooks/local/lib ] && cp -R hooks/local/lib "hooks/local/lib.pre-bootstrap-$TS" |
| 194 | + mkdir -p hooks/local/lib |
| 195 | + cp -R "$SOURCE_CLONE/hooks/local/lib/." hooks/local/lib/ |
| 196 | + fi |
| 197 | +) |
| 198 | +[ -f "$BREPO/hooks/local/lib/merge-module-size-baseline.sh" ] \ |
| 199 | + && ok "p1-bootstrap-stages-lib" \ |
| 200 | + || bad "p1-bootstrap-stages-lib" "bootstrap staging did not copy hooks/local/lib/ (P1 broken)" |
| 201 | +# And the bootstrap script itself must actually contain the staging block (guards |
| 202 | +# against the test passing while the script regressed). |
| 203 | +grep -q 'hooks/local/lib' "$BOOTSTRAP_SH" \ |
| 204 | + && ok "p1-bootstrap-references-lib" \ |
| 205 | + || bad "p1-bootstrap-references-lib" "bootstrap-upgrade.sh has no hooks/local/lib staging (P1 missing)" |
| 206 | + |
| 207 | +finish |
0 commit comments