Skip to content

Commit 28fe2ea

Browse files
Pavel Sherclaude
andcommitted
P4: add test-bootstrap-baseline-hop.sh (RED-then-GREEN adoption-hop proof) + wire into run-tests
RED arm: pre-fix local-only source logic loses the project baseline row. GREEN arm: the shipped source_merge_lib ($SOURCE_CLONE-first) preserves it and check-module-size --all passes. Plus P1 staging proof. Loud setup asserts so a missing fixture / silent skip fails the test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5324358 commit 28fe2ea

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

hooks/tests/run-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ run_shell_phase test-newline-preserve.sh "newline-preserve"
174174
run_shell_phase test-baseline-merge.sh "baseline-merge"
175175
run_shell_phase test-sync-allowlist.sh "sync-allowlist"
176176
run_shell_phase test-policy-state-preserve.sh "policy-state"
177+
run_shell_phase test-bootstrap-baseline-hop.sh "bootstrap-baseline-hop"
177178

178179
# Write report
179180
{
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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

Comments
 (0)