|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Tests for timestamp-based branch naming in create-new-feature.sh and common.sh |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +PASS_COUNT=0 |
| 6 | +FAIL_COUNT=0 |
| 7 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 8 | +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" |
| 9 | +CREATE_FEATURE="$PROJECT_ROOT/scripts/bash/create-new-feature.sh" |
| 10 | +COMMON_SH="$PROJECT_ROOT/scripts/bash/common.sh" |
| 11 | + |
| 12 | +# ── Helpers ────────────────────────────────────────────────────────────────── |
| 13 | + |
| 14 | +pass() { PASS_COUNT=$((PASS_COUNT + 1)); echo " PASS: $1"; } |
| 15 | +fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); echo " FAIL: $1"; } |
| 16 | + |
| 17 | +setup_temp_repo() { |
| 18 | + local dir |
| 19 | + dir=$(mktemp -d) |
| 20 | + git -C "$dir" init -q |
| 21 | + git -C "$dir" commit --allow-empty -m "init" -q |
| 22 | + # Copy scripts so the script can source common.sh |
| 23 | + mkdir -p "$dir/scripts/bash" |
| 24 | + cp "$CREATE_FEATURE" "$dir/scripts/bash/create-new-feature.sh" |
| 25 | + cp "$COMMON_SH" "$dir/scripts/bash/common.sh" |
| 26 | + # Create .specify dir so template resolution doesn't fail hard |
| 27 | + mkdir -p "$dir/.specify/templates" |
| 28 | + echo "$dir" |
| 29 | +} |
| 30 | + |
| 31 | +setup_temp_dir_no_git() { |
| 32 | + local dir |
| 33 | + dir=$(mktemp -d) |
| 34 | + mkdir -p "$dir/scripts/bash" |
| 35 | + cp "$CREATE_FEATURE" "$dir/scripts/bash/create-new-feature.sh" |
| 36 | + cp "$COMMON_SH" "$dir/scripts/bash/common.sh" |
| 37 | + mkdir -p "$dir/.specify/templates" |
| 38 | + echo "$dir" |
| 39 | +} |
| 40 | + |
| 41 | +cleanup() { rm -rf "$1"; } |
| 42 | + |
| 43 | +# ── Tests ──────────────────────────────────────────────────────────────────── |
| 44 | + |
| 45 | +test_1_timestamp_creates_branch() { |
| 46 | + echo "Test 1: --timestamp creates branch with YYYYMMDD-HHMMSS prefix" |
| 47 | + local repo; repo=$(setup_temp_repo) |
| 48 | + local output |
| 49 | + output=$(cd "$repo" && bash scripts/bash/create-new-feature.sh --timestamp --short-name user-auth 'Add user auth' 2>/dev/null) |
| 50 | + local branch |
| 51 | + branch=$(echo "$output" | grep "BRANCH_NAME:" | sed 's/BRANCH_NAME: //') |
| 52 | + if [[ "$branch" =~ ^[0-9]{8}-[0-9]{6}-user-auth$ ]]; then |
| 53 | + pass "timestamp branch matches expected pattern" |
| 54 | + else |
| 55 | + fail "expected YYYYMMDD-HHMMSS-user-auth, got: $branch" |
| 56 | + fi |
| 57 | + cleanup "$repo" |
| 58 | +} |
| 59 | + |
| 60 | +test_2_sequential_default_with_existing_specs() { |
| 61 | + echo "Test 2: Sequential default with existing specs" |
| 62 | + local repo; repo=$(setup_temp_repo) |
| 63 | + mkdir -p "$repo/specs/001-first-feat" "$repo/specs/002-second-feat" |
| 64 | + local output |
| 65 | + output=$(cd "$repo" && bash scripts/bash/create-new-feature.sh --short-name new-feat 'New feature' 2>/dev/null) |
| 66 | + local branch |
| 67 | + branch=$(echo "$output" | grep "BRANCH_NAME:" | sed 's/BRANCH_NAME: //') |
| 68 | + if [[ "$branch" =~ ^[0-9]{3}-new-feat$ ]]; then |
| 69 | + pass "sequential branch uses correct numbering" |
| 70 | + else |
| 71 | + fail "expected NNN-new-feat, got: $branch" |
| 72 | + fi |
| 73 | + cleanup "$repo" |
| 74 | +} |
| 75 | + |
| 76 | +test_3_number_and_timestamp_warns() { |
| 77 | + echo "Test 3: --number + --timestamp warns and uses timestamp" |
| 78 | + local repo; repo=$(setup_temp_repo) |
| 79 | + local stderr_output |
| 80 | + stderr_output=$(cd "$repo" && bash scripts/bash/create-new-feature.sh --timestamp --number 42 --short-name feat 'Feature' 2>&1 >/dev/null) |
| 81 | + if echo "$stderr_output" | grep -q "Warning.*--number.*ignored"; then |
| 82 | + pass "warning emitted for conflicting flags" |
| 83 | + else |
| 84 | + fail "expected warning about --number being ignored, got: $stderr_output" |
| 85 | + fi |
| 86 | + cleanup "$repo" |
| 87 | +} |
| 88 | + |
| 89 | +test_4_json_output_keys() { |
| 90 | + echo "Test 4: JSON output contains expected keys" |
| 91 | + local repo; repo=$(setup_temp_repo) |
| 92 | + local output |
| 93 | + output=$(cd "$repo" && bash scripts/bash/create-new-feature.sh --json --timestamp --short-name api 'API feature' 2>/dev/null) |
| 94 | + local has_all_keys=true |
| 95 | + for key in BRANCH_NAME SPEC_FILE FEATURE_NUM; do |
| 96 | + if ! echo "$output" | grep -q "\"$key\""; then |
| 97 | + has_all_keys=false |
| 98 | + break |
| 99 | + fi |
| 100 | + done |
| 101 | + if $has_all_keys; then |
| 102 | + pass "JSON output has BRANCH_NAME, SPEC_FILE, FEATURE_NUM" |
| 103 | + else |
| 104 | + fail "JSON missing expected keys: $output" |
| 105 | + fi |
| 106 | + cleanup "$repo" |
| 107 | +} |
| 108 | + |
| 109 | +test_5_long_name_truncation() { |
| 110 | + echo "Test 5: Long branch name is truncated to <= 244 chars" |
| 111 | + local repo; repo=$(setup_temp_repo) |
| 112 | + # Generate a very long name (300+ chars) |
| 113 | + local long_name |
| 114 | + long_name=$(python3 -c "print('a-' * 150 + 'end')") |
| 115 | + local output |
| 116 | + output=$(cd "$repo" && bash scripts/bash/create-new-feature.sh --timestamp --short-name "$long_name" 'Long feature' 2>/dev/null) |
| 117 | + local branch |
| 118 | + branch=$(echo "$output" | grep "BRANCH_NAME:" | sed 's/BRANCH_NAME: //') |
| 119 | + if [[ ${#branch} -le 244 ]] && [[ "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then |
| 120 | + pass "branch truncated to ${#branch} chars with timestamp prefix" |
| 121 | + else |
| 122 | + fail "branch length=${#branch}, expected <= 244 starting with timestamp" |
| 123 | + fi |
| 124 | + cleanup "$repo" |
| 125 | +} |
| 126 | + |
| 127 | +test_6_check_feature_branch_accepts_timestamp() { |
| 128 | + echo "Test 6: check_feature_branch accepts timestamp branch" |
| 129 | + source "$COMMON_SH" |
| 130 | + if check_feature_branch "20260319-143022-feat" "true" 2>/dev/null; then |
| 131 | + pass "timestamp branch accepted" |
| 132 | + else |
| 133 | + fail "timestamp branch rejected" |
| 134 | + fi |
| 135 | +} |
| 136 | + |
| 137 | +test_7_check_feature_branch_accepts_sequential() { |
| 138 | + echo "Test 7: check_feature_branch accepts sequential branch" |
| 139 | + source "$COMMON_SH" |
| 140 | + if check_feature_branch "004-feat" "true" 2>/dev/null; then |
| 141 | + pass "sequential branch accepted" |
| 142 | + else |
| 143 | + fail "sequential branch rejected" |
| 144 | + fi |
| 145 | +} |
| 146 | + |
| 147 | +test_8_check_feature_branch_rejects_main() { |
| 148 | + echo "Test 8: check_feature_branch rejects main" |
| 149 | + source "$COMMON_SH" |
| 150 | + if check_feature_branch "main" "true" 2>/dev/null; then |
| 151 | + fail "main branch should be rejected" |
| 152 | + else |
| 153 | + pass "main branch correctly rejected" |
| 154 | + fi |
| 155 | +} |
| 156 | + |
| 157 | +test_9_check_feature_branch_rejects_partial_timestamp() { |
| 158 | + echo "Test 9: check_feature_branch rejects 7-digit date" |
| 159 | + source "$COMMON_SH" |
| 160 | + if check_feature_branch "2026031-143022-feat" "true" 2>/dev/null; then |
| 161 | + fail "7-digit date should be rejected" |
| 162 | + else |
| 163 | + pass "partial timestamp correctly rejected" |
| 164 | + fi |
| 165 | +} |
| 166 | + |
| 167 | +test_10_find_feature_dir_by_prefix_timestamp() { |
| 168 | + echo "Test 10: find_feature_dir_by_prefix with timestamp branch" |
| 169 | + source "$COMMON_SH" |
| 170 | + local dir; dir=$(mktemp -d) |
| 171 | + mkdir -p "$dir/specs/20260319-143022-user-auth" |
| 172 | + local result |
| 173 | + result=$(find_feature_dir_by_prefix "$dir" "20260319-143022-user-auth") |
| 174 | + if [[ "$result" == "$dir/specs/20260319-143022-user-auth" ]]; then |
| 175 | + pass "found correct spec dir for timestamp branch" |
| 176 | + else |
| 177 | + fail "expected $dir/specs/20260319-143022-user-auth, got: $result" |
| 178 | + fi |
| 179 | + cleanup "$dir" |
| 180 | +} |
| 181 | + |
| 182 | +test_11_find_feature_dir_by_prefix_cross_branch() { |
| 183 | + echo "Test 11: find_feature_dir_by_prefix cross-branch (different suffix, same timestamp)" |
| 184 | + source "$COMMON_SH" |
| 185 | + local dir; dir=$(mktemp -d) |
| 186 | + mkdir -p "$dir/specs/20260319-143022-original-feat" |
| 187 | + local result |
| 188 | + result=$(find_feature_dir_by_prefix "$dir" "20260319-143022-different-name") |
| 189 | + if [[ "$result" == "$dir/specs/20260319-143022-original-feat" ]]; then |
| 190 | + pass "cross-branch prefix lookup found existing dir" |
| 191 | + else |
| 192 | + fail "expected $dir/specs/20260319-143022-original-feat, got: $result" |
| 193 | + fi |
| 194 | + cleanup "$dir" |
| 195 | +} |
| 196 | + |
| 197 | +test_12_get_current_branch_env_var() { |
| 198 | + echo "Test 12: get_current_branch returns SPECIFY_FEATURE env var" |
| 199 | + source "$COMMON_SH" |
| 200 | + local result |
| 201 | + result=$(SPECIFY_FEATURE="my-custom-branch" get_current_branch) |
| 202 | + if [[ "$result" == "my-custom-branch" ]]; then |
| 203 | + pass "SPECIFY_FEATURE env var used" |
| 204 | + else |
| 205 | + fail "expected my-custom-branch, got: $result" |
| 206 | + fi |
| 207 | +} |
| 208 | + |
| 209 | +test_13_no_git_timestamp() { |
| 210 | + echo "Test 13: No-git repo + timestamp creates spec dir with warning" |
| 211 | + local dir; dir=$(setup_temp_dir_no_git) |
| 212 | + local stderr_output |
| 213 | + stderr_output=$(cd "$dir" && bash scripts/bash/create-new-feature.sh --timestamp --short-name no-git-feat 'No git feature' 2>&1 >/dev/null) |
| 214 | + # Check spec dir was created |
| 215 | + local spec_dirs |
| 216 | + spec_dirs=$(ls "$dir/specs/" 2>/dev/null || echo "") |
| 217 | + if [[ -n "$spec_dirs" ]] && echo "$stderr_output" | grep -qi "git.*not detected\|warning"; then |
| 218 | + pass "spec dir created and warning emitted for no-git" |
| 219 | + else |
| 220 | + fail "spec_dirs='$spec_dirs', stderr='$stderr_output'" |
| 221 | + fi |
| 222 | + cleanup "$dir" |
| 223 | +} |
| 224 | + |
| 225 | +test_14_e2e_timestamp_flow() { |
| 226 | + echo "Test 14: E2E timestamp flow" |
| 227 | + local repo; repo=$(setup_temp_repo) |
| 228 | + source "$COMMON_SH" |
| 229 | + # Create feature with timestamp |
| 230 | + cd "$repo" |
| 231 | + bash scripts/bash/create-new-feature.sh --timestamp --short-name e2e-ts 'E2E timestamp test' 2>/dev/null >/dev/null |
| 232 | + # Check branch was created |
| 233 | + local current_branch |
| 234 | + current_branch=$(git -C "$repo" rev-parse --abbrev-ref HEAD) |
| 235 | + if [[ "$current_branch" =~ ^[0-9]{8}-[0-9]{6}-e2e-ts$ ]]; then |
| 236 | + # Check spec dir exists |
| 237 | + if [[ -d "$repo/specs/$current_branch" ]]; then |
| 238 | + # Check branch passes validation |
| 239 | + if check_feature_branch "$current_branch" "true" 2>/dev/null; then |
| 240 | + pass "E2E timestamp: branch, dir, and validation all pass" |
| 241 | + else |
| 242 | + fail "E2E timestamp: branch validation failed for $current_branch" |
| 243 | + fi |
| 244 | + else |
| 245 | + fail "E2E timestamp: spec dir not found for $current_branch" |
| 246 | + fi |
| 247 | + else |
| 248 | + fail "E2E timestamp: branch doesn't match pattern, got: $current_branch" |
| 249 | + fi |
| 250 | + cleanup "$repo" |
| 251 | +} |
| 252 | + |
| 253 | +test_15_e2e_sequential_flow() { |
| 254 | + echo "Test 15: E2E sequential flow (regression guard)" |
| 255 | + local repo; repo=$(setup_temp_repo) |
| 256 | + source "$COMMON_SH" |
| 257 | + cd "$repo" |
| 258 | + bash scripts/bash/create-new-feature.sh --short-name seq-feat 'Sequential feature' 2>/dev/null >/dev/null |
| 259 | + local current_branch |
| 260 | + current_branch=$(git -C "$repo" rev-parse --abbrev-ref HEAD) |
| 261 | + if [[ "$current_branch" =~ ^[0-9]{3}-seq-feat$ ]]; then |
| 262 | + if [[ -d "$repo/specs/$current_branch" ]]; then |
| 263 | + if check_feature_branch "$current_branch" "true" 2>/dev/null; then |
| 264 | + pass "E2E sequential: branch, dir, and validation all pass" |
| 265 | + else |
| 266 | + fail "E2E sequential: branch validation failed for $current_branch" |
| 267 | + fi |
| 268 | + else |
| 269 | + fail "E2E sequential: spec dir not found for $current_branch" |
| 270 | + fi |
| 271 | + else |
| 272 | + fail "E2E sequential: branch doesn't match pattern, got: $current_branch" |
| 273 | + fi |
| 274 | + cleanup "$repo" |
| 275 | +} |
| 276 | + |
| 277 | +# ── Run all tests ──────────────────────────────────────────────────────────── |
| 278 | + |
| 279 | +echo "=== Timestamp Branch Naming Tests ===" |
| 280 | +echo "" |
| 281 | + |
| 282 | +test_1_timestamp_creates_branch |
| 283 | +test_2_sequential_default_with_existing_specs |
| 284 | +test_3_number_and_timestamp_warns |
| 285 | +test_4_json_output_keys |
| 286 | +test_5_long_name_truncation |
| 287 | +test_6_check_feature_branch_accepts_timestamp |
| 288 | +test_7_check_feature_branch_accepts_sequential |
| 289 | +test_8_check_feature_branch_rejects_main |
| 290 | +test_9_check_feature_branch_rejects_partial_timestamp |
| 291 | +test_10_find_feature_dir_by_prefix_timestamp |
| 292 | +test_11_find_feature_dir_by_prefix_cross_branch |
| 293 | +test_12_get_current_branch_env_var |
| 294 | +test_13_no_git_timestamp |
| 295 | +test_14_e2e_timestamp_flow |
| 296 | +test_15_e2e_sequential_flow |
| 297 | + |
| 298 | +echo "" |
| 299 | +echo "=== Results: $PASS_COUNT passed, $FAIL_COUNT failed ===" |
| 300 | + |
| 301 | +if [[ $FAIL_COUNT -gt 0 ]]; then |
| 302 | + exit 1 |
| 303 | +fi |
| 304 | +exit 0 |
0 commit comments