|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Fixture for #358: close-task.sh must resolve checkpoint.sh, never print it bare. |
| 3 | +# |
| 4 | +# usage: test-close-task-checkpoint.sh /path/to/project-bin/close-task.sh |
| 5 | +# |
| 6 | +# checkpoint.sh lives in the toolkit at claude-hooks/bin/ and reaches ~/.claude/bin only via |
| 7 | +# the opt-in `bin/install-claude-hooks.sh --basic`. The old "### Then" block printed |
| 8 | +# `checkpoint.sh --write` unconditionally, so a project without the hooks got a command that |
| 9 | +# did not exist. Lookup order: PATH, ~/.claude/bin, $MXTK_ROOT/claude-hooks/bin. |
| 10 | +# |
| 11 | +# T1 found on PATH -> prints that full path |
| 12 | +# T2 found in ~/.claude/bin -> prints that full path (fake HOME) |
| 13 | +# T3 found in toolkit -> prints $MXTK_ROOT/claude-hooks/bin/checkpoint.sh |
| 14 | +# T4 PATH beats ~/.claude/bin and toolkit |
| 15 | +# T5 not found -> install pointer, rest of the block still printed, |
| 16 | +# exit code unchanged (0 on a clean project) |
| 17 | +# T6 not found, writes owed -> still exit 1 (owed-write semantics kept) |
| 18 | +# T7 paths with a space (and a ') in HOME / toolkit -> printed single-quoted, and the printed |
| 19 | +# command actually runs when pasted into a shell |
| 20 | +# T8 the claude-hooks/bin/close-task.sh copy prints its sibling checkpoint.sh full path, quoted |
| 21 | + |
| 22 | +set -uo pipefail |
| 23 | + |
| 24 | +SUT="${1:?usage: test-close-task-checkpoint.sh /path/to/project-bin/close-task.sh}" |
| 25 | +case "$SUT" in /*) ;; *) SUT="$PWD/$SUT" ;; esac |
| 26 | +[ -f "$SUT" ] || { echo "no such file: $SUT" >&2; exit 2; } |
| 27 | + |
| 28 | +TMP=$(mktemp -d) |
| 29 | +trap 'rm -rf "$TMP"' EXIT |
| 30 | + |
| 31 | +pass=0; fail=0 |
| 32 | +ok() { pass=$((pass+1)); echo " PASS $1"; } |
| 33 | +bad() { fail=$((fail+1)); echo " FAIL $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ | /'; } |
| 34 | + |
| 35 | +# A project that is not a git repo, with the script installed as bin/close-task.sh (no |
| 36 | +# toolkit next to it, so the "installed-from" fallback cannot resolve a toolkit root). |
| 37 | +PROJ="$TMP/proj"; mkdir -p "$PROJ/bin" |
| 38 | +cp "$SUT" "$PROJ/bin/close-task.sh"; chmod +x "$PROJ/bin/close-task.sh" |
| 39 | + |
| 40 | +# A clean PATH: system tools only, no checkpoint.sh. |
| 41 | +SYSPATH="/usr/bin:/bin" |
| 42 | +mk_ckpt() { mkdir -p "$1"; printf '#!/bin/sh\nexit 0\n' > "$1/checkpoint.sh"; chmod +x "$1/checkpoint.sh"; } |
| 43 | + |
| 44 | +EMPTYHOME="$TMP/home-empty"; mkdir -p "$EMPTYHOME" |
| 45 | +NOTK="$TMP/no-toolkit"; mkdir -p "$NOTK" |
| 46 | + |
| 47 | +run() { # $1=HOME $2=PATH $3=MXTK_ROOT [args...] |
| 48 | + local h="$1" p="$2" m="$3"; shift 3 |
| 49 | + ( cd "$PROJ" && env -i HOME="$h" PATH="$p" MXTK_ROOT="$m" GIT_CEILING_DIRECTORIES="$TMP" \ |
| 50 | + bash "$PROJ/bin/close-task.sh" "$@" 2>&1 ) |
| 51 | +} |
| 52 | + |
| 53 | +echo "T1 found on PATH" |
| 54 | +mk_ckpt "$TMP/pathbin" |
| 55 | +out=$(run "$EMPTYHOME" "$TMP/pathbin:$SYSPATH" "$NOTK") |
| 56 | +grep -qF " '$TMP/pathbin/checkpoint.sh' --write" <<<"$out" && ok "T1 prints PATH hit" || bad "T1 prints PATH hit" "$(grep -n -A2 '### Then' <<<"$out")" |
| 57 | + |
| 58 | +echo "T2 found in ~/.claude/bin" |
| 59 | +H2="$TMP/home2"; mk_ckpt "$H2/.claude/bin" |
| 60 | +out=$(run "$H2" "$SYSPATH" "$NOTK") |
| 61 | +grep -qF " '$H2/.claude/bin/checkpoint.sh' --write" <<<"$out" && ok "T2 prints ~/.claude/bin hit" || bad "T2 prints ~/.claude/bin hit" "$(grep -n -A2 '### Then' <<<"$out")" |
| 62 | + |
| 63 | +echo "T3 found in toolkit claude-hooks/bin" |
| 64 | +TK="$TMP/toolkit"; mk_ckpt "$TK/claude-hooks/bin" |
| 65 | +out=$(run "$EMPTYHOME" "$SYSPATH" "$TK") |
| 66 | +grep -qF " '$TK/claude-hooks/bin/checkpoint.sh' --write" <<<"$out" && ok "T3 prints toolkit hit" || bad "T3 prints toolkit hit" "$(grep -n -A2 '### Then' <<<"$out")" |
| 67 | + |
| 68 | +echo "T4 lookup order" |
| 69 | +out=$(run "$H2" "$TMP/pathbin:$SYSPATH" "$TK") |
| 70 | +grep -qF " '$TMP/pathbin/checkpoint.sh' --write" <<<"$out" && ok "T4a PATH wins" || bad "T4a PATH wins" "$(grep -n -A2 '### Then' <<<"$out")" |
| 71 | +out=$(run "$H2" "$SYSPATH" "$TK") |
| 72 | +grep -qF " '$H2/.claude/bin/checkpoint.sh' --write" <<<"$out" && ok "T4b ~/.claude/bin beats toolkit" || bad "T4b ~/.claude/bin beats toolkit" "$(grep -n -A2 '### Then' <<<"$out")" |
| 73 | + |
| 74 | +echo "T5 not found" |
| 75 | +out=$(run "$EMPTYHOME" "$SYSPATH" "$NOTK"); rc=$? |
| 76 | +grep -qF "checkpoint.sh not installed — run '$NOTK/bin/install-claude-hooks.sh' --basic, or skip: RESUME.md is the resume source" <<<"$out" \ |
| 77 | + && ok "T5a install pointer printed" || bad "T5a install pointer printed" "$(grep -n -A2 '### Then' <<<"$out")" |
| 78 | +grep -qE '^ +checkpoint\.sh --write' <<<"$out" && bad "T5b no bare checkpoint.sh line" "$(grep -n 'checkpoint.sh --write' <<<"$out")" || ok "T5b no bare checkpoint.sh line" |
| 79 | +grep -qF 'OVERWRITE docs/progress/RESUME.md' <<<"$out" && grep -qF 'Shared toolkit:' <<<"$out" \ |
| 80 | + && ok "T5c rest of the block still printed" || bad "T5c rest of the block still printed" |
| 81 | +[ "$rc" = 0 ] && ok "T5d exit 0 on a clean project" || bad "T5d exit 0 on a clean project (got $rc)" |
| 82 | + |
| 83 | +echo "T6 not found, writes owed" |
| 84 | +run "$EMPTYHOME" "$SYSPATH" "$NOTK" --add "exec 010-foo.mdl" >/dev/null |
| 85 | +out=$(run "$EMPTYHOME" "$SYSPATH" "$NOTK"); rc=$? |
| 86 | +[ "$rc" = 1 ] && ok "T6 exit 1 when writes owed" || bad "T6 exit 1 when writes owed (got $rc)" |
| 87 | + |
| 88 | +echo "T7 paths with spaces" |
| 89 | +# Run the printed line through a shell: the path must resolve to the stub we made. |
| 90 | +runs_line() { local line; line=$(grep -m1 -E "^ '.*' --write" <<<"$1") || return 1 |
| 91 | + ( eval "${line%%#*}" ) >/dev/null 2>&1; } |
| 92 | +H7="$TMP/John Doe"; mk_ckpt "$H7/.claude/bin" |
| 93 | +out=$(run "$H7" "$SYSPATH" "$NOTK") |
| 94 | +grep -qF " '$H7/.claude/bin/checkpoint.sh' --write" <<<"$out" && runs_line "$out" \ |
| 95 | + && ok "T7a space in HOME: quoted and runnable" || bad "T7a space in HOME: quoted and runnable" "$(grep -n -A1 '### Then' <<<"$out")" |
| 96 | +TK7="$TMP/my tool's kit"; mk_ckpt "$TK7/claude-hooks/bin" |
| 97 | +out=$(run "$EMPTYHOME" "$SYSPATH" "$TK7") |
| 98 | +grep -qF " '$TMP/my tool'\\''s kit/claude-hooks/bin/checkpoint.sh' --write" <<<"$out" && runs_line "$out" \ |
| 99 | + && ok "T7b space and ' in toolkit path: quoted and runnable" || bad "T7b space and ' in toolkit path: quoted and runnable" "$(grep -n -A1 '### Then' <<<"$out")" |
| 100 | +TK7N="$TMP/no kit here"; mkdir -p "$TK7N" |
| 101 | +out=$(run "$EMPTYHOME" "$SYSPATH" "$TK7N") |
| 102 | +grep -qF "run '$TK7N/bin/install-claude-hooks.sh' --basic" <<<"$out" \ |
| 103 | + && ok "T7c install hint quoted" || bad "T7c install hint quoted" "$(grep -n -A1 '### Then' <<<"$out")" |
| 104 | + |
| 105 | +echo "T8 claude-hooks copy" |
| 106 | +HOOKS="$(cd "$(dirname "$SUT")/.." && pwd)/claude-hooks/bin/close-task.sh" |
| 107 | +if [ -f "$HOOKS" ]; then |
| 108 | + B8="$H7/.claude/bin"; cp "$HOOKS" "$B8/close-task.sh" |
| 109 | + out=$( cd "$PROJ" && env -i HOME="$H7" PATH="$SYSPATH" GIT_CEILING_DIRECTORIES="$TMP" bash "$B8/close-task.sh" 2>&1 ) |
| 110 | + grep -qF " '$B8/checkpoint.sh' --write" <<<"$out" && runs_line "$out" \ |
| 111 | + && ok "T8 sibling path printed, quoted" || bad "T8 sibling path printed, quoted" "$(grep -n -A1 '### Then' <<<"$out")" |
| 112 | +else |
| 113 | + bad "T8 claude-hooks/bin/close-task.sh not found next to $SUT" |
| 114 | +fi |
| 115 | + |
| 116 | +echo |
| 117 | +echo "test-close-task-checkpoint: $pass passed, $fail failed" |
| 118 | +[ "$fail" = 0 ] |
0 commit comments