#!/usr/bin/env bash # validate-tdd.sh # Guard that production code changes are accompanied by tests. set -euo pipefail BASE_REF="${1:-origin/main}" if ! git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then BASE_REF="HEAD~1" fi CHANGED_FILES=() while IFS= read -r line; do [[ -n "$line" ]] && CHANGED_FILES+=("$line") done < <(git diff --name-only "$BASE_REF"...HEAD) if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then echo "[validate-tdd] No changed files." exit 0 fi is_production_file() { local f="$1" [[ "$f" == shared/src/commonMain/* ]] && return 0 [[ "$f" == androidApp/src/main/* ]] && return 0 [[ "$f" == iosApp/iosApp/* ]] && [[ "$f" != iosApp/iosAppUITests/* ]] && [[ "$f" != iosApp/iosAppTests/* ]] && return 0 return 1 } is_test_file() { local f="$1" [[ "$f" == shared/src/commonTest/* ]] && return 0 [[ "$f" == shared/src/jvmTest/* ]] && return 0 [[ "$f" == androidApp/src/androidTest/* ]] && return 0 [[ "$f" == androidApp/src/test/* ]] && return 0 [[ "$f" == iosApp/iosAppUITests/* ]] && return 0 [[ "$f" == iosApp/iosAppTests/* ]] && return 0 return 1 } PROD_COUNT=0 TEST_COUNT=0 for file in "${CHANGED_FILES[@]}"; do if is_production_file "$file"; then PROD_COUNT=$((PROD_COUNT + 1)) fi if is_test_file "$file"; then TEST_COUNT=$((TEST_COUNT + 1)) fi done if [[ "$PROD_COUNT" -gt 0 && "$TEST_COUNT" -eq 0 ]]; then echo "[validate-tdd] Failing: production code changed without matching test updates." echo "[validate-tdd] Production files changed: $PROD_COUNT" exit 1 fi CHANGED_TEST_FILES=() TEST_PATH_REGEX='^(shared/src/(commonTest|jvmTest)/|androidApp/src/(androidTest|test)/|iosApp/iosApp(UI)?Tests/)' while IFS= read -r line; do [[ -n "$line" ]] && CHANGED_TEST_FILES+=("$line") done < <( if command -v rg >/dev/null 2>&1; then printf '%s\n' "${CHANGED_FILES[@]}" | rg "$TEST_PATH_REGEX" || true else printf '%s\n' "${CHANGED_FILES[@]}" | grep -E "$TEST_PATH_REGEX" || true fi ) for test_file in "${CHANGED_TEST_FILES[@]:-}"; do if [[ -f "$test_file" ]]; then if command -v rg >/dev/null 2>&1; then if rg -q 'catch[[:space:]]*\([[:space:]]*AssertionError|XCTExpectFailure|@Ignore|@Disabled' "$test_file"; then echo "[validate-tdd] Failing: potential weak assertion/skip anti-pattern in $test_file" exit 1 fi else if grep -Eq 'catch[[:space:]]*\([[:space:]]*AssertionError|XCTExpectFailure|@Ignore|@Disabled' "$test_file"; then echo "[validate-tdd] Failing: potential weak assertion/skip anti-pattern in $test_file" exit 1 fi fi fi done if [[ "${FORCE_AUDIT_GATES:-0}" == "1" ]]; then echo "[validate-tdd] FORCE_AUDIT_GATES enabled." fi echo "[validate-tdd] PASS ($BASE_REF...HEAD)"