|
| 1 | +#!/usr/bin/env bash |
| 2 | +# test-restatement-git-hooks.sh — #504: the restatement guard's last-writer ledger only saw exec.sh's |
| 3 | +# writes, so a `git pull` / `git checkout` that brought newer model work in left it saying "05 wrote |
| 4 | +# last" and a replay of 05 over 09's work stayed quiet. install-project-hooks.sh now installs |
| 5 | +# post-merge + post-checkout hooks that append a RESTORE row when the operation changed the model. |
| 6 | +# |
| 7 | +# Usage: bash tests/wave2/test-restatement-git-hooks.sh [path-to-install-project-hooks.sh] |
| 8 | +# script-owners.sh and _common.sh are taken from the same directory. |
| 9 | +# Real git (merge, fast-forward pull, branch checkout, file checkout), both layouts (.mpr at the |
| 10 | +# root, and two-tree app/App.mpr), a pre-existing foreign hook chained. No mxcli, no mxbuild. |
| 11 | +set -uo pipefail |
| 12 | +TK="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" |
| 13 | +SUBJECT="${1:-$TK/project-bin/install-project-hooks.sh}" |
| 14 | +case "$SUBJECT" in /*) ;; *) SUBJECT="$PWD/$SUBJECT" ;; esac |
| 15 | +BIN="$(dirname "$SUBJECT")" |
| 16 | +WORK="$(mktemp -d "${TMPDIR:-/tmp}/restate-hooks.XXXXXX")" || exit 2 |
| 17 | +trap 'rm -rf "$WORK"' EXIT |
| 18 | +PASS=0; FAIL=0 |
| 19 | +ok() { PASS=$((PASS+1)); echo " ok — $1"; } |
| 20 | +bad() { FAIL=$((FAIL+1)); echo " FAIL — $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ /'; } |
| 21 | +export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t |
| 22 | +export MODEL_UNVERIFIED_OK=1 # the pre-commit gate hook is not what this fixture tests |
| 23 | +DOC="MICROFLOW Sales.ACT_Reject" |
| 24 | + |
| 25 | +run_layout() { # $1 = label, $2 = model dir relative to the root ("" or "app") |
| 26 | + local L="$1" MD="$2" R="$WORK/$1" C="$WORK/$1-clone" lg out mdir cm |
| 27 | + echo "== layout: $L" |
| 28 | + mkdir -p "$R/bin"; cd "$R" || return |
| 29 | + git init -q -b main . |
| 30 | + cp "$BIN/_common.sh" "$BIN/install-project-hooks.sh" "$BIN/script-owners.sh" "$BIN/model-stamp.sh" bin/ 2>/dev/null |
| 31 | + mdir="$R${MD:+/$MD}"; mkdir -p "$mdir/mprcontents/aa" |
| 32 | + echo v1 > "$mdir/App.mpr"; echo u1 > "$mdir/mprcontents/aa/unit.mxunit"; echo doc > README.md |
| 33 | + printf '.claude/\n' > .gitignore |
| 34 | + git add -A >/dev/null; git commit -qm base |
| 35 | + # The clone is the local machine: it ran 05, so its ledger says 05 wrote last. |
| 36 | + git clone -q "$R" "$C"; cd "$C" || return |
| 37 | + git config user.name t; git config user.email t@t |
| 38 | + mkdir -p .claude/loop/script-owners; lg="$C/.claude/loop/script-owners/written.tsv" |
| 39 | + printf '100\tmdlsource/05-reject.mdl\t%s\n' "$DOC" > "$lg" |
| 40 | + printf '#!/usr/bin/env bash\necho foreign >> "%s/foreign.log"\n' "$WORK" > .git/hooks/post-merge; chmod +x .git/hooks/post-merge |
| 41 | + ./bin/install-project-hooks.sh >"$WORK/$L-install.out" 2>&1 || bad "$L: install failed" "$(cat "$WORK/$L-install.out")" |
| 42 | + ./bin/install-project-hooks.sh --check >/dev/null 2>&1 && ok "$L: --check sees all hooks" || bad "$L: --check" "$(./bin/install-project-hooks.sh --check 2>&1)" |
| 43 | + [ -f .git/hooks/post-merge.pre-mxtk ] && ok "$L: foreign post-merge kept and chained" || bad "$L: foreign hook not kept" |
| 44 | + out="$(PROJECT_ROOT="$C" bash bin/script-owners.sh last-writer "$DOC")" |
| 45 | + [ "$out" = "mdlsource/05-reject.mdl" ] && ok "$L: precondition — ledger says 05 wrote last" || bad "$L: precondition '$out'" |
| 46 | + |
| 47 | + # 1. a pull that touches no model file: no marker |
| 48 | + cd "$R"; echo more >> README.md; git commit -qam docs; cd "$C" |
| 49 | + git pull -q --no-rebase origin main >/dev/null 2>&1 |
| 50 | + grep -q RESTORE "$lg" && bad "$L: docs-only pull marked the ledger" || ok "$L: docs-only pull leaves the ledger alone" |
| 51 | + grep -q foreign "$WORK/foreign.log" 2>/dev/null && ok "$L: chained foreign hook ran" || bad "$L: chained foreign hook did not run" |
| 52 | + |
| 53 | + # 2. #504's repro: upstream carries 09's work on the model; pull brings it in |
| 54 | + cd "$R"; echo "v2 (09 work)" > "$mdir/mprcontents/aa/unit.mxunit"; git commit -qam "09 work"; cd "$C" |
| 55 | + git pull -q --no-rebase origin main >/dev/null 2>&1 |
| 56 | + grep -q 'RESTORE git-post-merge:' "$lg" && ok "$L: model-changing pull appends a RESTORE row" || bad "$L: no RESTORE row after pull" "$(cat "$lg")" |
| 57 | + out="$(PROJECT_ROOT="$C" bash bin/script-owners.sh last-writer "$DOC")" |
| 58 | + [ -z "$out" ] && ok "$L: last-writer forgets 05 → the next replay check asks the model" || bad "$L: last-writer still '$out'" |
| 59 | + |
| 60 | + # 3. branch checkout: same model → nothing; different model → marker |
| 61 | + printf '200\tmdlsource/05-reject.mdl\t%s\n' "$DOC" >> "$lg"; n0="$(grep -c RESTORE "$lg")" |
| 62 | + git checkout -q -b side; n1="$(grep -c RESTORE "$lg")" |
| 63 | + [ "$n0" = "$n1" ] && ok "$L: checkout to a branch at the same commit adds nothing" || bad "$L: same-commit checkout marked" |
| 64 | + cm="$C${MD:+/$MD}" |
| 65 | + echo "side" > "$cm/App.mpr"; git commit -qam side; git checkout -q main |
| 66 | + [ "$(grep -c RESTORE "$lg")" -gt "$n1" ] && ok "$L: checkout that changes the .mpr marks" || bad "$L: model-changing checkout did not mark" |
| 67 | + # 4. file checkout (the documented remedy `git checkout HEAD -- mprcontents/`): always marks |
| 68 | + n2="$(grep -c RESTORE "$lg")"; echo dirty > "$cm/mprcontents/aa/unit.mxunit" |
| 69 | + git checkout -q HEAD -- "${MD:+$MD/}mprcontents" |
| 70 | + [ "$(grep -c RESTORE "$lg")" -gt "$n2" ] && ok "$L: file checkout marks (hook gets no paths)" || bad "$L: file checkout did not mark" |
| 71 | + [ "$(cat "$cm/mprcontents/aa/unit.mxunit")" = "v2 (09 work)" ] && ok "$L: file checkout restored the unit" || bad "$L: unit not restored" |
| 72 | + # 5. no ledger: never created by the hook |
| 73 | + rm -f "$lg"; git checkout -q side; git checkout -q main |
| 74 | + [ ! -f "$lg" ] && ok "$L: no ledger → hook creates none" || bad "$L: hook created a ledger" |
| 75 | + # 6. idempotent re-install |
| 76 | + ./bin/install-project-hooks.sh >/dev/null 2>&1 |
| 77 | + [ ! -f .git/hooks/post-merge.pre-mxtk.pre-mxtk ] && grep -c 'mxtk-restatement-mark' .git/hooks/post-merge | grep -qx 1 \ |
| 78 | + && ok "$L: re-install is idempotent" || bad "$L: re-install re-chained" |
| 79 | +} |
| 80 | + |
| 81 | +run_layout single "" |
| 82 | +run_layout twotree "app" |
| 83 | +echo "== $PASS passed, $FAIL failed" |
| 84 | +[ "$FAIL" -eq 0 ] |
0 commit comments