From 8af4d7f42b9eab50fc32fe8578d2895316404b23 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 22:22:48 +0000 Subject: [PATCH 1/4] feat(09-01): unify hook into single trigger with language-agnostic detection - Replace 3 per-language code paths (JS/Java/Python ~320 lines) with unified architecture - Add detect_any_config() that checks all config types at each directory level - Add find_codeflash_binary() with venv -> PATH -> uv run -> npx resolution - Add detect_changed_languages() for language-aware NOT-CONFIGURED setup messages - Single codeflash --subagent trigger for configured projects regardless of language - Reduce script from 463 lines to 297 lines Co-Authored-By: Claude Opus 4.6 --- scripts/suggest-optimize.sh | 381 ++++++++++-------------------------- 1 file changed, 108 insertions(+), 273 deletions(-) diff --git a/scripts/suggest-optimize.sh b/scripts/suggest-optimize.sh index e5dd987..93b6c36 100755 --- a/scripts/suggest-optimize.sh +++ b/scripts/suggest-optimize.sh @@ -81,43 +81,43 @@ echo "$COMMIT_HASH" >> "$SEEN_MARKER" # --- From here on, we know there are new commits to optimize --- -# Walk from $PWD upward to $REPO_ROOT looking for project config. -# Sets: PROJECT_TYPE, PROJECT_DIR, PROJECT_CONFIG_PATH, PROJECT_CONFIGURED -detect_project() { - PROJECT_TYPE="" - PROJECT_DIR="" - PROJECT_CONFIG_PATH="" +# Source find-venv.sh for Python venv detection +# shellcheck disable=SC1091 +source "$(dirname "$0")/find-venv.sh" + +# Walk from $PWD upward to $REPO_ROOT checking ALL config types at each level. +# Sets: PROJECT_CONFIGURED, FOUND_CONFIGS (space-separated), PROJECT_DIR +detect_any_config() { PROJECT_CONFIGURED="false" + FOUND_CONFIGS="" + PROJECT_DIR="" local search_dir="$PWD" while true; do - # Check codeflash.toml first (Java projects) + # Check codeflash.toml (Java projects) if [ -f "$search_dir/codeflash.toml" ]; then - PROJECT_TYPE="java" - PROJECT_DIR="$search_dir" - PROJECT_CONFIG_PATH="$search_dir/codeflash.toml" if grep -q '\[tool\.codeflash\]' "$search_dir/codeflash.toml" 2>/dev/null; then PROJECT_CONFIGURED="true" + FOUND_CONFIGS="${FOUND_CONFIGS:+$FOUND_CONFIGS }codeflash.toml" + [ -z "$PROJECT_DIR" ] && PROJECT_DIR="$search_dir" fi - break fi + # Check pyproject.toml (Python projects) if [ -f "$search_dir/pyproject.toml" ]; then - PROJECT_TYPE="python" - PROJECT_DIR="$search_dir" - PROJECT_CONFIG_PATH="$search_dir/pyproject.toml" if grep -q '\[tool\.codeflash\]' "$search_dir/pyproject.toml" 2>/dev/null; then PROJECT_CONFIGURED="true" + FOUND_CONFIGS="${FOUND_CONFIGS:+$FOUND_CONFIGS }pyproject.toml" + [ -z "$PROJECT_DIR" ] && PROJECT_DIR="$search_dir" fi - break fi + # Check package.json (JS/TS projects) if [ -f "$search_dir/package.json" ]; then - PROJECT_TYPE="js" - PROJECT_DIR="$search_dir" - PROJECT_CONFIG_PATH="$search_dir/package.json" if jq -e '.codeflash' "$search_dir/package.json" >/dev/null 2>&1; then PROJECT_CONFIGURED="true" + FOUND_CONFIGS="${FOUND_CONFIGS:+$FOUND_CONFIGS }package.json" + [ -z "$PROJECT_DIR" ] && PROJECT_DIR="$search_dir" fi - break fi + # Move to parent directory if [ "$search_dir" = "$REPO_ROOT" ]; then break fi @@ -133,133 +133,70 @@ detect_project() { done } -# Discover project config -detect_project - -CHECK_DIR="${PROJECT_DIR:-$PWD}" - -# --- JS/TS project path --------------------------------------------------- -if [ "$PROJECT_TYPE" = "js" ]; then - INSTALL_CMD="npm install --save-dev codeflash" - - # Check if codeflash npm package is available +# Unified binary resolution: venv -> PATH -> uv run -> npx +# Sets: CODEFLASH_BIN, CODEFLASH_INSTALLED +find_codeflash_binary() { + CODEFLASH_BIN="" CODEFLASH_INSTALLED="false" - if (cd "$CHECK_DIR" && npx codeflash --version >/dev/null 2>&1); then + # a. Active venv + if [ -n "${VIRTUAL_ENV:-}" ] && [ -x "${VIRTUAL_ENV}/bin/codeflash" ]; then + CODEFLASH_BIN="${VIRTUAL_ENV}/bin/codeflash" CODEFLASH_INSTALLED="true" + return fi - - # Not configured → prompt for config (+ install if needed) - if [ "$PROJECT_CONFIGURED" != "true" ]; then - if [ -n "$PROJECT_CONFIG_PATH" ]; then - TARGET_MSG="Add a \`\"codeflash\"\` key to \`$PROJECT_CONFIG_PATH\`" - else - TARGET_MSG="Create \`$REPO_ROOT/package.json\` (via \`npm init -y\`) and add a \`\"codeflash\"\` key" - fi - - INSTALL_STEP="" - if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_STEP=" -- First, install codeflash by running \`${INSTALL_CMD}\` in \`${CHECK_DIR}\`." - fi - - SETUP_MSG="JS/TS files were changed in a recent commit, but codeflash is not configured yet. - -Set up codeflash to optimize the JavaScript/TypeScript code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user — discover the correct paths yourself): -1. The relative path with respect to package.json to the root of the JS/TS module — typically \".\" for the root directory or \"src\". -2. The relative path with respect to package.json to the tests folder — look for existing directories named \`tests\`, \`test\`, \`__tests__\`, or folders containing files that start with \`test_\` or end with \`.test.js\`/\`.spec.ts\`. If none exist, default to \`tests\`. - -Once determined:${INSTALL_STEP} -- If the tests folder does not exist, create it with mkdir -p. -- ${TARGET_MSG}. Read the existing package.json, parse it, add/update the \"codeflash\" key with the discovered values, and write back with 2-space indentation: - -{ - \"codeflash\": { - \"moduleRoot\": \"\", - \"testsRoot\": \"\", - \"formatterCmds\": [\"disabled\"], - \"ignorePaths\": [\"dist\", \"**/node_modules\", \"**/__tests__\"] - } -} - -Once the configuration is written, run \`npx codeflash --subagent\` as a background Bash task (run_in_background: true)." - - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - SETUP_MSG="${SETUP_MSG} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - jq -nc --arg reason "$SETUP_MSG" '{"decision": "block", "reason": $reason}' - exit 0 + # b. PATH + if command -v codeflash >/dev/null 2>&1; then + CODEFLASH_BIN="codeflash" + CODEFLASH_INSTALLED="true" + return fi - - # Configured but not installed - if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_MSG="JS/TS files were changed in a recent commit, but the codeflash npm package is not installed. - -Install codeflash to optimize the JavaScript/TypeScript code for performance by running: - - ${INSTALL_CMD} - -in \`${CHECK_DIR}\`, then run \`npx codeflash --subagent\` as a background Bash task (run_in_background: true)." - - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - INSTALL_MSG="${INSTALL_MSG} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - jq -nc --arg reason "$INSTALL_MSG" '{"decision": "block", "reason": $reason}' - exit 0 + # c. uv run + if uv run codeflash --version >/dev/null 2>&1; then + CODEFLASH_BIN="uv run codeflash" + CODEFLASH_INSTALLED="true" + return fi - - # Build codeflash command for JS/TS — must run from project root - if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then - RUN_CMD="cd $PROJECT_DIR && npx codeflash --subagent" - else - RUN_CMD="npx codeflash --subagent" + # d. npx + if npx codeflash --version >/dev/null 2>&1; then + CODEFLASH_BIN="npx codeflash" + CODEFLASH_INSTALLED="true" + return fi +} - MESSAGE="JS/TS files were changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the JavaScript/TypeScript code for performance." - - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - MESSAGE="${MESSAGE} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." +# Parse changed files to detect which languages have changes. +# Sets: CHANGED_LANGS (space-separated: python java javascript) +detect_changed_languages() { + CHANGED_LANGS="" + if echo "$CHANGED_COMMITS" | grep -q '\.py$'; then + CHANGED_LANGS="python" + fi + if echo "$CHANGED_COMMITS" | grep -q '\.java$'; then + CHANGED_LANGS="${CHANGED_LANGS:+$CHANGED_LANGS }java" fi + if echo "$CHANGED_COMMITS" | grep -qE '\.(js|ts|jsx|tsx)$'; then + CHANGED_LANGS="${CHANGED_LANGS:+$CHANGED_LANGS }javascript" + fi +} - jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason}' - exit 0 -fi +# ---- Main flow ---- -# --- Java project path ----------------------------------------------------- -if [ "$PROJECT_TYPE" = "java" ]; then - INSTALL_CMD="pip install codeflash" +detect_any_config - # Find codeflash binary: check PATH first, then uv run (Java projects don't use venvs) - CODEFLASH_BIN="" - CODEFLASH_INSTALLED="false" - if command -v codeflash >/dev/null 2>&1; then - CODEFLASH_BIN="codeflash" - CODEFLASH_INSTALLED="true" - elif uv run codeflash --version >/dev/null 2>&1; then - CODEFLASH_BIN="uv run codeflash" - CODEFLASH_INSTALLED="true" - fi +if [ "$PROJECT_CONFIGURED" = "true" ]; then + find_codeflash_binary - # Not installed if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_MSG="Java files were changed in a recent commit, but codeflash is not installed. + # Show install instructions (language-agnostic) + INSTALL_MSG="Code was changed in a recent commit, but codeflash is not installed. -Install codeflash to optimize the Java code for performance by running: +Install codeflash to optimize your code for performance: - ${INSTALL_CMD} + pip install codeflash Then run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." + # Append auto-allow if needed if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then INSTALL_MSG="${INSTALL_MSG} @@ -270,30 +207,14 @@ Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTING exit 0 fi - # Not configured - if [ "$PROJECT_CONFIGURED" != "true" ]; then - SETUP_MSG="Java files were changed in a recent commit, but codeflash is not configured yet. - -Run \`${CODEFLASH_BIN} init --yes\` in \`${CHECK_DIR}\` to automatically detect and configure the Java project, then run \`${CODEFLASH_BIN} --subagent\` as a background Bash task (run_in_background: true) to optimize the code for performance." - - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - SETUP_MSG="${SETUP_MSG} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - jq -nc --arg reason "$SETUP_MSG" '{"decision": "block", "reason": $reason}' - exit 0 - fi - - # Everything ready — build codeflash command for Java + # Build run command if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then RUN_CMD="cd $PROJECT_DIR && $CODEFLASH_BIN --subagent" else RUN_CMD="$CODEFLASH_BIN --subagent" fi - MESSAGE="Java files were changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the Java code for performance." + MESSAGE="Code was changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the code for performance." if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then MESSAGE="${MESSAGE} @@ -305,34 +226,22 @@ Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTING exit 0 fi -# --- Python project path --------------------------------------------------- -INSTALL_CMD="pip install codeflash" - -# Only use codeflash from the current virtual environment. -# If no venv is active, try to find and activate one automatically. -# shellcheck disable=SC1091 -source "$(dirname "$0")/find-venv.sh" - -if [ -z "${VIRTUAL_ENV:-}" ]; then - # No venv found — instruct Claude to create one, install codeflash, configure, and run +# --- NOT CONFIGURED path: language-aware setup instructions --- +detect_changed_languages - # Build config setup instructions if not configured - PYPROJECT_SETUP="" - if [ "$PROJECT_CONFIGURED" != "true" ]; then - if [ -n "$PROJECT_CONFIG_PATH" ]; then - TARGET_MSG="Append to \`$PROJECT_CONFIG_PATH\`" - else - TARGET_MSG="Create \`$REPO_ROOT/pyproject.toml\`" - fi +for LANG in $CHANGED_LANGS; do + # Build per-language setup message + case "$LANG" in + python) + SETUP_MSG="Python files were changed in a recent commit, but codeflash is not configured yet. - PYPROJECT_SETUP=" -3. Automatically determine the following two values by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user — discover the correct paths yourself): - a. The relative path with respect to pyproject.toml to the root of the Python module — the module root is where tests import from. for example, if the module root is abc/ then the tests would be importing code as \`from abc import xyz\`. - b. The relative path with respect to pyproject.toml to the tests folder — look for existing directories named \`tests\` or \`test\`, or folders containing files that start with \`test_\`. If none exist, default to \`tests\`. +Set up codeflash to optimize the Python code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user -- discover the correct paths yourself): +1. The relative path with respect to pyproject.toml to the root of the Python module -- the module root is where tests import from. for example, if the module root is abc/ then the tests would be importing code as \`from abc import xyz\`. +2. The relative path with respect to pyproject.toml to the tests folder -- look for existing directories named \`tests\` or \`test\`, or folders containing files that start with \`test_\`. If none exist, default to \`tests\`. -4. Once determined: - - If the tests folder does not exist, create it with mkdir -p. - - ${TARGET_MSG} (create the file if it does not exist), adding the following section with the discovered values substituted: +Once determined: +- If the tests folder does not exist, create it with mkdir -p. +- Create or update \`pyproject.toml\` adding the following section with the discovered values substituted: [tool.codeflash] # All paths are relative to this pyproject.toml's directory. @@ -341,75 +250,37 @@ tests-root = \"\" ignore-paths = [] formatter-cmds = [\"disabled\"] -5. Run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - else - PYPROJECT_SETUP=" -3. Run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - fi - - # Build auto-allow setup step if not already configured - SETUP_PERMISSIONS_STEP="" - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - SETUP_PERMISSIONS_STEP=" -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - VENV_MSG="Python files were changed in a recent commit, but no Python virtual environment was found. - -Set up codeflash to optimize the Python code for performance. Perform the following steps: - -1. Create a virtual environment by running: \`python3 -m venv .venv\` in \`$REPO_ROOT\`, then activate it by running: \`source $REPO_ROOT/.venv/bin/activate\`. +Once the configuration is written, run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." + ;; + java) + SETUP_MSG="Java files were changed in a recent commit, but codeflash is not configured yet. -2. Install codeflash by running: \`${INSTALL_CMD}\`. -${PYPROJECT_SETUP} -${SETUP_PERMISSIONS_STEP}" +Run \`codeflash init --yes\` to automatically detect and configure the Java project, then run \`codeflash --subagent\` as a background Bash task (run_in_background: true) to optimize the code for performance." + ;; + javascript) + SETUP_MSG="JS/TS files were changed in a recent commit, but codeflash is not configured yet. - jq -nc --arg reason "$VENV_MSG" '{"decision": "block", "reason": $reason}' - exit 0 -fi - -CODEFLASH_BIN="${VIRTUAL_ENV}/bin/codeflash" - -# Check if codeflash is installed in the venv -CODEFLASH_INSTALLED="false" -if [ -x "$CODEFLASH_BIN" ] && "$CODEFLASH_BIN" --version >/dev/null 2>&1; then - CODEFLASH_INSTALLED="true" -fi +Set up codeflash to optimize the JavaScript/TypeScript code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user -- discover the correct paths yourself): +1. The relative path with respect to package.json to the root of the JS/TS module -- typically \".\" for the root directory or \"src\". +2. The relative path with respect to package.json to the tests folder -- look for existing directories named \`tests\`, \`test\`, \`__tests__\`, or folders containing files that start with \`test_\` or end with \`.test.js\`/\`.spec.ts\`. If none exist, default to \`tests\`. -# Check if codeflash is configured in this project -if [ "$PROJECT_CONFIGURED" != "true" ]; then - # Build a human-friendly target path for the setup message - if [ -n "$PROJECT_CONFIG_PATH" ]; then - TARGET_MSG="Append to \`$PROJECT_CONFIG_PATH\`" - else - TARGET_MSG="Create \`$REPO_ROOT/pyproject.toml\`" - fi - - # Include install step if codeflash is not installed - INSTALL_STEP="" - if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_STEP=" -- First, install codeflash by running \`${INSTALL_CMD}\` in \`${CHECK_DIR}\`." - fi - - SETUP_MSG="Python files were changed in a recent commit, but codeflash is not configured yet. - -Set up codeflash to optimize the Python code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user — discover the correct paths yourself): -1. The relative path with respect to pyproject.toml to the root of the Python module — the module root is where tests import from. for example, if the module root is abc/ then the tests would be importing code as \`from abc import xyz\`. -2. The relative path with respect to pyproject.toml to the tests folder — look for existing directories named \`tests\` or \`test\`, or folders containing files that start with \`test_\`. If none exist, default to \`tests\`. - -Once determined:${INSTALL_STEP} +Once determined: - If the tests folder does not exist, create it with mkdir -p. -- ${TARGET_MSG} (create the file if it does not exist), adding the following section with the discovered values substituted: +- Add/update the \"codeflash\" key in \`package.json\`: -[tool.codeflash] -# All paths are relative to this pyproject.toml's directory. -module-root = \"\" -tests-root = \"\" -ignore-paths = [] -formatter-cmds = [\"disabled\"] +{ + \"codeflash\": { + \"moduleRoot\": \"\", + \"testsRoot\": \"\", + \"formatterCmds\": [\"disabled\"], + \"ignorePaths\": [\"dist\", \"**/node_modules\", \"**/__tests__\"] + } +} -Once the pyproject.toml configuration is written, run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." +Once the configuration is written, run \`npx codeflash --subagent\` as a background Bash task (run_in_background: true)." + ;; + *) continue ;; + esac # Append auto-allow setup instructions if not already configured if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then @@ -420,43 +291,7 @@ Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTING jq -nc --arg reason "$SETUP_MSG" '{"decision": "block", "reason": $reason}' exit 0 -fi - -# Codeflash is configured but not installed in the venv -if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_MSG="Python files were changed in a recent commit, but codeflash is not installed in the active virtual environment (\`$VIRTUAL_ENV\`). - -Install codeflash to optimize the Python code for performance by running: - - ${INSTALL_CMD} - -in \`${CHECK_DIR}\`, then run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - INSTALL_MSG="${INSTALL_MSG} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - jq -nc --arg reason "$INSTALL_MSG" '{"decision": "block", "reason": $reason}' - exit 0 -fi - -# Instruct Claude to run codeflash as a background subagent -if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then - RUN_CMD="cd $PROJECT_DIR && $CODEFLASH_BIN --subagent" -else - RUN_CMD="$CODEFLASH_BIN --subagent" -fi - -MESSAGE="Python files were changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the Python code for performance." - -# Append auto-allow setup instructions if not already configured -if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - MESSAGE="${MESSAGE} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." -fi +done -jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason}' +# No recognized languages in changed files -- exit silently +exit 0 From 55211e7afb1d7e7a4dbb6ad88313d8faf0db95b0 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 19 Mar 2026 02:03:31 +0000 Subject: [PATCH 2/4] test(10-01): add guard pattern and bats test suite for cc-plugin hook - Restructure suggest-optimize.sh: move function definitions above guard, wrap preamble and main flow in BASH_SOURCE guard for testability - Create test_helper.bash with shared setup/teardown and load_hook_functions - Create suggest_optimize.bats with 19 tests covering: - detect_any_config: codeflash.toml, pyproject.toml, package.json, multiple configs, missing configs, skipping without codeflash section - find_codeflash_binary: PATH, venv, not installed, venv-over-PATH priority - detect_changed_languages: python, java, javascript (js/ts/jsx/tsx), mixed, unrecognized files Co-Authored-By: Claude Opus 4.6 --- scripts/suggest-optimize.sh | 168 ++++++++++++++++--------------- tests/suggest_optimize.bats | 193 ++++++++++++++++++++++++++++++++++++ tests/test_helper.bash | 21 ++++ 3 files changed, 300 insertions(+), 82 deletions(-) create mode 100644 tests/suggest_optimize.bats create mode 100644 tests/test_helper.bash diff --git a/scripts/suggest-optimize.sh b/scripts/suggest-optimize.sh index 93b6c36..83c25b0 100755 --- a/scripts/suggest-optimize.sh +++ b/scripts/suggest-optimize.sh @@ -3,88 +3,6 @@ # user if they'd like to run codeflash to optimize their code. # Fires when Claude is about to finish its response. -set -euo pipefail - -LOGFILE="/tmp/codeflash-hook-debug.log" -exec 2>>"$LOGFILE" -set -x - -# Read stdin (Stop hook pipes context as JSON via stdin) -INPUT=$(cat) - -# If the stop hook is already active (Claude already responded to a previous block), -# allow the stop to proceed to avoid an infinite block loop. -STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false") -if [ "$STOP_HOOK_ACTIVE" = "true" ]; then - exit 0 -fi - -## Per-project tracker keyed on repo root (resolve symlinks so PWD and REPO_ROOT share a prefix) -REPO_ROOT=$(cd "$(git rev-parse --show-toplevel 2>/dev/null)" && pwd -P) || exit 0 -cd "$(pwd -P)" - -# --- Check if codeflash is already auto-allowed in .claude/settings.json --- -CODEFLASH_AUTO_ALLOWED="false" -SETTINGS_JSON="$REPO_ROOT/.claude/settings.json" -if [ -f "$SETTINGS_JSON" ]; then - if jq -e '.permissions.allow // [] | any(test("codeflash"))' "$SETTINGS_JSON" >/dev/null 2>&1; then - CODEFLASH_AUTO_ALLOWED="true" - fi -fi - -# --- Detect new commits with Python/Java/JS/TS files since session started --- - -# Extract transcript_path from hook input to determine session start time -TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true) -if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then - exit 0 -fi - -# Get the transcript file's creation (birth) time as the session start timestamp. -# This predates any commits Claude could have made in this session. -get_file_birth_time() { - local file="$1" - if [[ "$(uname)" == "Darwin" ]]; then - stat -f %B "$file" - else - local btime - btime=$(stat -c %W "$file" 2>/dev/null || echo "0") - if [ "$btime" = "0" ] || [ -z "$btime" ]; then - stat -c %Y "$file" - else - echo "$btime" - fi - fi -} - -SESSION_START=$(get_file_birth_time "$TRANSCRIPT_PATH") -if [ -z "$SESSION_START" ] || [ "$SESSION_START" = "0" ]; then - exit 0 -fi - -# Find commits with Python/Java/JS/TS files made after the session started -CHANGED_COMMITS=$(git log --after="@$SESSION_START" --name-only --diff-filter=ACMR --pretty=format: -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | sort -u | grep -v '^$' || true) -if [ -z "$CHANGED_COMMITS" ]; then - exit 0 -fi - -# Dedup: don't trigger twice for the same set of changes across sessions. -# Use the project directory from transcript_path for state storage. -TRANSCRIPT_DIR=$(dirname "$TRANSCRIPT_PATH") -SEEN_MARKER="$TRANSCRIPT_DIR/codeflash-seen" - -COMMIT_HASH=$(git log --after="@$SESSION_START" --pretty=format:%H -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | shasum -a 256 | cut -d' ' -f1) -if [ -f "$SEEN_MARKER" ] && grep -qF "$COMMIT_HASH" "$SEEN_MARKER" 2>/dev/null; then - exit 0 -fi -echo "$COMMIT_HASH" >> "$SEEN_MARKER" - -# --- From here on, we know there are new commits to optimize --- - -# Source find-venv.sh for Python venv detection -# shellcheck disable=SC1091 -source "$(dirname "$0")/find-venv.sh" - # Walk from $PWD upward to $REPO_ROOT checking ALL config types at each level. # Sets: PROJECT_CONFIGURED, FOUND_CONFIGS (space-separated), PROJECT_DIR detect_any_config() { @@ -180,6 +98,90 @@ detect_changed_languages() { } # ---- Main flow ---- +# Guard: only run when executed directly (not sourced for testing) +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + +set -euo pipefail + +LOGFILE="/tmp/codeflash-hook-debug.log" +exec 2>>"$LOGFILE" +set -x + +# Read stdin (Stop hook pipes context as JSON via stdin) +INPUT=$(cat) + +# If the stop hook is already active (Claude already responded to a previous block), +# allow the stop to proceed to avoid an infinite block loop. +STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false") +if [ "$STOP_HOOK_ACTIVE" = "true" ]; then + exit 0 +fi + +## Per-project tracker keyed on repo root (resolve symlinks so PWD and REPO_ROOT share a prefix) +REPO_ROOT=$(cd "$(git rev-parse --show-toplevel 2>/dev/null)" && pwd -P) || exit 0 +cd "$(pwd -P)" + +# --- Check if codeflash is already auto-allowed in .claude/settings.json --- +CODEFLASH_AUTO_ALLOWED="false" +SETTINGS_JSON="$REPO_ROOT/.claude/settings.json" +if [ -f "$SETTINGS_JSON" ]; then + if jq -e '.permissions.allow // [] | any(test("codeflash"))' "$SETTINGS_JSON" >/dev/null 2>&1; then + CODEFLASH_AUTO_ALLOWED="true" + fi +fi + +# --- Detect new commits with Python/Java/JS/TS files since session started --- + +# Extract transcript_path from hook input to determine session start time +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true) +if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then + exit 0 +fi + +# Get the transcript file's creation (birth) time as the session start timestamp. +# This predates any commits Claude could have made in this session. +get_file_birth_time() { + local file="$1" + if [[ "$(uname)" == "Darwin" ]]; then + stat -f %B "$file" + else + local btime + btime=$(stat -c %W "$file" 2>/dev/null || echo "0") + if [ "$btime" = "0" ] || [ -z "$btime" ]; then + stat -c %Y "$file" + else + echo "$btime" + fi + fi +} + +SESSION_START=$(get_file_birth_time "$TRANSCRIPT_PATH") +if [ -z "$SESSION_START" ] || [ "$SESSION_START" = "0" ]; then + exit 0 +fi + +# Find commits with Python/Java/JS/TS files made after the session started +CHANGED_COMMITS=$(git log --after="@$SESSION_START" --name-only --diff-filter=ACMR --pretty=format: -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | sort -u | grep -v '^$' || true) +if [ -z "$CHANGED_COMMITS" ]; then + exit 0 +fi + +# Dedup: don't trigger twice for the same set of changes across sessions. +# Use the project directory from transcript_path for state storage. +TRANSCRIPT_DIR=$(dirname "$TRANSCRIPT_PATH") +SEEN_MARKER="$TRANSCRIPT_DIR/codeflash-seen" + +COMMIT_HASH=$(git log --after="@$SESSION_START" --pretty=format:%H -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | shasum -a 256 | cut -d' ' -f1) +if [ -f "$SEEN_MARKER" ] && grep -qF "$COMMIT_HASH" "$SEEN_MARKER" 2>/dev/null; then + exit 0 +fi +echo "$COMMIT_HASH" >> "$SEEN_MARKER" + +# --- From here on, we know there are new commits to optimize --- + +# Source find-venv.sh for Python venv detection +# shellcheck disable=SC1091 +source "$(dirname "$0")/find-venv.sh" detect_any_config @@ -295,3 +297,5 @@ done # No recognized languages in changed files -- exit silently exit 0 + +fi # End guard diff --git a/tests/suggest_optimize.bats b/tests/suggest_optimize.bats new file mode 100644 index 0000000..2d9bde2 --- /dev/null +++ b/tests/suggest_optimize.bats @@ -0,0 +1,193 @@ +#!/usr/bin/env bats + +load test_helper + +# --- detect_any_config tests --- + +@test "detect_any_config finds codeflash.toml" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src/main/java"\n' > "$TEST_DIR/codeflash.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"codeflash.toml"* ]] + [ "$PROJECT_DIR" = "$TEST_DIR" ] +} + +@test "detect_any_config finds pyproject.toml" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src"\n' > "$TEST_DIR/pyproject.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"pyproject.toml"* ]] +} + +@test "detect_any_config finds package.json with codeflash key" { + mkdir -p "$TEST_DIR" + echo '{"codeflash": {"moduleRoot": "src"}}' > "$TEST_DIR/package.json" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"package.json"* ]] +} + +@test "detect_any_config finds multiple config types" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src"\n' > "$TEST_DIR/pyproject.toml" + printf '[tool.codeflash]\nmodule-root = "src/main/java"\n' > "$TEST_DIR/codeflash.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"pyproject.toml"* ]] + [[ "$FOUND_CONFIGS" == *"codeflash.toml"* ]] +} + +@test "detect_any_config returns false when no config found" { + mkdir -p "$TEST_DIR" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "false" ] + [ -z "$FOUND_CONFIGS" ] +} + +@test "detect_any_config skips package.json without codeflash key" { + mkdir -p "$TEST_DIR" + echo '{"name": "my-project", "version": "1.0.0"}' > "$TEST_DIR/package.json" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "false" ] +} + +@test "detect_any_config skips pyproject.toml without codeflash section" { + mkdir -p "$TEST_DIR" + printf '[tool.black]\nline-length = 120\n' > "$TEST_DIR/pyproject.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "false" ] +} + +@test "detect_any_config finds all three config types" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src"\n' > "$TEST_DIR/pyproject.toml" + printf '[tool.codeflash]\nmodule-root = "src/main/java"\n' > "$TEST_DIR/codeflash.toml" + echo '{"codeflash": {"moduleRoot": "src"}}' > "$TEST_DIR/package.json" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"codeflash.toml"* ]] + [[ "$FOUND_CONFIGS" == *"pyproject.toml"* ]] + [[ "$FOUND_CONFIGS" == *"package.json"* ]] +} + +# --- find_codeflash_binary tests --- + +@test "find_codeflash_binary finds binary in PATH" { + mkdir -p "$TEST_DIR/bin" + printf '#!/bin/bash\necho "codeflash 1.0"\n' > "$TEST_DIR/bin/codeflash" + chmod +x "$TEST_DIR/bin/codeflash" + export PATH="$TEST_DIR/bin:$PATH" + load_hook_functions + find_codeflash_binary + [ "$CODEFLASH_INSTALLED" = "true" ] + [ "$CODEFLASH_BIN" = "codeflash" ] +} + +@test "find_codeflash_binary finds binary in venv" { + mkdir -p "$TEST_DIR/venv/bin" + printf '#!/bin/bash\necho "codeflash 1.0"\n' > "$TEST_DIR/venv/bin/codeflash" + chmod +x "$TEST_DIR/venv/bin/codeflash" + export VIRTUAL_ENV="$TEST_DIR/venv" + load_hook_functions + find_codeflash_binary + [ "$CODEFLASH_INSTALLED" = "true" ] + [ "$CODEFLASH_BIN" = "$TEST_DIR/venv/bin/codeflash" ] +} + +@test "find_codeflash_binary reports not installed when missing" { + load_hook_functions + # Save PATH and use an empty directory so codeflash/uv/npx are all unavailable + local saved_path="$PATH" + mkdir -p "$TEST_DIR/empty_bin" + export PATH="$TEST_DIR/empty_bin" + unset VIRTUAL_ENV + hash -r 2>/dev/null || true + find_codeflash_binary + # Restore PATH before assertions so teardown can work + export PATH="$saved_path" + [ "$CODEFLASH_INSTALLED" = "false" ] + [ -z "$CODEFLASH_BIN" ] +} + +@test "find_codeflash_binary prefers venv over PATH" { + mkdir -p "$TEST_DIR/venv/bin" + printf '#!/bin/bash\necho "venv codeflash"\n' > "$TEST_DIR/venv/bin/codeflash" + chmod +x "$TEST_DIR/venv/bin/codeflash" + mkdir -p "$TEST_DIR/bin" + printf '#!/bin/bash\necho "path codeflash"\n' > "$TEST_DIR/bin/codeflash" + chmod +x "$TEST_DIR/bin/codeflash" + export VIRTUAL_ENV="$TEST_DIR/venv" + export PATH="$TEST_DIR/bin:$PATH" + load_hook_functions + find_codeflash_binary + [ "$CODEFLASH_INSTALLED" = "true" ] + [ "$CODEFLASH_BIN" = "$TEST_DIR/venv/bin/codeflash" ] +} + +# --- detect_changed_languages tests --- + +@test "detect_changed_languages detects python" { + export CHANGED_COMMITS="src/main.py +tests/test_utils.py" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"python"* ]] +} + +@test "detect_changed_languages detects java" { + export CHANGED_COMMITS="src/Main.java" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"java"* ]] +} + +@test "detect_changed_languages detects javascript from ts and jsx" { + export CHANGED_COMMITS="src/App.tsx +src/utils.js" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} + +@test "detect_changed_languages detects mixed languages" { + export CHANGED_COMMITS="src/main.py +src/Main.java +src/app.ts" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"python"* ]] + [[ "$CHANGED_LANGS" == *"java"* ]] + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} + +@test "detect_changed_languages returns empty for no recognized files" { + export CHANGED_COMMITS="README.md +Makefile" + load_hook_functions + detect_changed_languages + [ -z "$CHANGED_LANGS" ] +} + +@test "detect_changed_languages detects js from .jsx files" { + export CHANGED_COMMITS="src/Component.jsx" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} + +@test "detect_changed_languages detects js from .tsx files" { + export CHANGED_COMMITS="src/Page.tsx" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} diff --git a/tests/test_helper.bash b/tests/test_helper.bash new file mode 100644 index 0000000..5fe0750 --- /dev/null +++ b/tests/test_helper.bash @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Shared setup for suggest-optimize.sh bats tests + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOOK_SCRIPT="$SCRIPT_DIR/scripts/suggest-optimize.sh" + +setup() { + TEST_DIR="$(mktemp -d)" + export REPO_ROOT="$TEST_DIR" + cd "$TEST_DIR" || return 1 +} + +teardown() { + rm -rf "$TEST_DIR" +} + +# Source only the function definitions (guard prevents main flow) +load_hook_functions() { + source "$HOOK_SCRIPT" +} From 3871b7137dc53cd3d2e8b995a90107bf3ef927df Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 19 Mar 2026 03:55:24 +0000 Subject: [PATCH 3/4] fix: update integration tests for unified hook architecture - Set CHECK_DIR before sourcing find-venv.sh (moved after detect_any_config) - Update integration tests to match unified hook behavior: - "NOT installed" tests use restricted PATH with mock uv/npx - JS tests assert "codeflash --subagent" instead of "npx codeflash --subagent" - No-venv tests assert install prompt instead of "create venv" prompt - Not-configured tests no longer embed install assertions - Add not_installed_path() and setup_mock_uv_no_codeflash() test helpers All 51 tests pass (19 unit + 25 integration + 7 find-venv). Co-Authored-By: Claude Opus 4.6 --- scripts/suggest-optimize.sh | 9 +- tests/helpers/setup.bash | 22 +++++ tests/test_suggest_optimize.bats | 152 +++++++++++++++---------------- 3 files changed, 98 insertions(+), 85 deletions(-) diff --git a/scripts/suggest-optimize.sh b/scripts/suggest-optimize.sh index e4e9bd9..eafb5b9 100755 --- a/scripts/suggest-optimize.sh +++ b/scripts/suggest-optimize.sh @@ -208,10 +208,6 @@ echo "$COMMIT_HASH" >> "$SEEN_MARKER" # --- From here on, we know there are new commits to optimize --- -# Source find-venv.sh for Python venv detection -# shellcheck disable=SC1091 -source "$(dirname "$0")/find-venv.sh" - # OAuth login script path OAUTH_SCRIPT="$(dirname "$0")/oauth-login.sh" @@ -225,6 +221,11 @@ fi detect_any_config +# Source find-venv.sh for Python venv detection (requires CHECK_DIR and REPO_ROOT) +CHECK_DIR="${PROJECT_DIR:-$PWD}" +# shellcheck disable=SC1091 +source "$(dirname "$0")/find-venv.sh" + if [ "$PROJECT_CONFIGURED" = "true" ]; then find_codeflash_binary diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index 4302c5c..43ab615 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -216,6 +216,28 @@ MOCK chmod +x "$MOCK_BIN/npx" } +# Create a mock uv binary in MOCK_BIN that fails for codeflash. +# This prevents find_codeflash_binary from finding codeflash via `uv run`. +setup_mock_uv_no_codeflash() { + mkdir -p "$MOCK_BIN" + cat > "$MOCK_BIN/uv" << 'MOCK' +#!/bin/bash +if [[ "$1" == "run" && "$2" == "codeflash" ]]; then + exit 1 +fi +exit 127 +MOCK + chmod +x "$MOCK_BIN/uv" +} + +# Minimal PATH for "not installed" tests: mock bin + system essentials only. +# Prevents finding codeflash via host uv/npx/PATH. +not_installed_path() { + setup_mock_uv_no_codeflash + setup_mock_npx false + echo "$MOCK_BIN:/usr/bin:/bin" +} + # --------------------------------------------------------------------------- # Hook runner # --------------------------------------------------------------------------- diff --git a/tests/test_suggest_optimize.bats b/tests/test_suggest_optimize.bats index 2e74e80..d726e0d 100755 --- a/tests/test_suggest_optimize.bats +++ b/tests/test_suggest_optimize.bats @@ -147,8 +147,10 @@ setup() { add_python_commit create_pyproject true create_fake_venv "$REPO/.venv" false + local restricted_path + restricted_path=$(not_installed_path) - run run_hook false "VIRTUAL_ENV=$REPO/.venv" + run run_hook false "VIRTUAL_ENV=$REPO/.venv" "PATH=$restricted_path" assert_block assert_reason_contains "pip install codeflash" } @@ -173,12 +175,12 @@ setup() { # Setup: pyproject.toml without [tool.codeflash]. Fake venv WITHOUT codeflash # binary. VIRTUAL_ENV set. -# Validates: When both installation and configuration are missing, the hook -# should instruct Claude to both install codeflash and set up the -# config. The install step is embedded within the setup instructions. -# Expected: Block with reason containing both "[tool.codeflash]" (setup) and -# "install codeflash" (installation). -@test "python: NOT configured + NOT installed → setup + install prompt" { +# Validates: When configuration is missing, the unified hook takes the +# NOT CONFIGURED path with per-language setup instructions. +# The setup message includes the [tool.codeflash] config template. +# Expected: Block with reason containing "[tool.codeflash]" (config template) +# and "module-root" (config field). +@test "python: NOT configured + NOT installed → setup prompt" { add_python_commit create_pyproject false create_fake_venv "$REPO/.venv" false @@ -186,44 +188,41 @@ setup() { run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block assert_reason_contains "[tool.codeflash]" - assert_reason_contains "install codeflash" + assert_reason_contains "module-root" } # Setup: pyproject.toml with [tool.codeflash]. No .venv or venv directory -# anywhere. VIRTUAL_ENV not set. -# Validates: Without a virtual environment, codeflash cannot run. The hook -# should instruct the user to create a venv, install codeflash in it, -# and restart Claude Code from within the activated environment. -# Expected: Block with reason containing "virtual environment" and -# "python3 -m venv" (the venv creation command). -@test "python: no venv + configured → create venv prompt" { +# anywhere. VIRTUAL_ENV not set. No codeflash in PATH/uv/npx. +# Validates: The unified hook uses find_codeflash_binary which checks venv, +# PATH, uv run, and npx. When none are found, it shows a generic +# "not installed" message with install instructions. +# Expected: Block with reason containing "pip install codeflash". +@test "python: no venv + configured → install prompt" { add_python_commit create_pyproject true + local restricted_path + restricted_path=$(not_installed_path) # No venv created, no VIRTUAL_ENV set - run run_hook false + run run_hook false "PATH=$restricted_path" assert_block - assert_reason_contains "virtual environment" - assert_reason_contains "python3 -m venv" + assert_reason_contains "pip install codeflash" } # Setup: pyproject.toml WITHOUT [tool.codeflash]. No venv anywhere. # VIRTUAL_ENV not set. -# Validates: The worst case — nothing is set up. The hook should instruct the -# user to create a venv, install codeflash, AND set up the config. -# The setup instructions (with [tool.codeflash] template) are included -# alongside the venv creation steps. -# Expected: Block with reason containing "virtual environment", -# "python3 -m venv", and "[tool.codeflash]". -@test "python: no venv + NOT configured → create venv + setup prompt" { +# Validates: When nothing is set up, the unified hook takes the NOT CONFIGURED +# path with per-language setup instructions including the config +# template. Install is handled implicitly when the user tries to run. +# Expected: Block with reason containing "[tool.codeflash]" and "module-root". +@test "python: no venv + NOT configured → setup prompt" { add_python_commit create_pyproject false run run_hook false assert_block - assert_reason_contains "virtual environment" - assert_reason_contains "python3 -m venv" assert_reason_contains "[tool.codeflash]" + assert_reason_contains "module-root" } # Setup: pyproject.toml with [tool.codeflash]. Fake venv at $REPO/.venv with @@ -231,14 +230,14 @@ setup() { # Validates: The hook sources find-venv.sh which searches CHECK_DIR/.venv, # CHECK_DIR/venv, REPO_ROOT/.venv, REPO_ROOT/venv for an activate # script. It should find .venv, activate it (setting VIRTUAL_ENV), -# and then proceed as if the venv was active from the start. +# and then find_codeflash_binary picks up the venv binary. # Expected: Block with reason containing "codeflash --subagent" (same as the # happy path — auto-discovery is transparent). @test "python: auto-discovers .venv when VIRTUAL_ENV not set" { add_python_commit create_pyproject true create_fake_venv "$REPO/.venv" true - # Don't pass VIRTUAL_ENV — script should find .venv itself + # Don't pass VIRTUAL_ENV — find-venv.sh should discover .venv run run_hook false assert_block @@ -252,10 +251,10 @@ setup() { # Setup: package.json with "codeflash" config key. Mock npx that returns # success for `codeflash --version`. PATH includes mock bin. # One .js file committed after session start. No pyproject.toml. -# Validates: The JS "happy path" — package.json is configured, codeflash npm -# package is available via npx. The hook instructs Claude to run -# `npx codeflash --subagent` in the background. -# Expected: Block with reason containing "npx codeflash --subagent" and +# Validates: The JS "happy path" — package.json is configured, codeflash is +# available. The unified hook instructs Claude to run +# `codeflash --subagent` in the background. +# Expected: Block with reason containing "codeflash --subagent" and # "run_in_background". @test "js: configured + codeflash installed → run codeflash" { add_js_commit @@ -264,33 +263,33 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" assert_reason_contains "run_in_background" } # Setup: package.json with "codeflash" key. Mock npx returns failure for # `codeflash --version` (package not installed). One .js commit. -# Validates: When codeflash is configured in package.json but the npm package -# is not installed, the hook should prompt to install it as a dev -# dependency before running. -# Expected: Block with reason containing "npm install --save-dev codeflash". +# Validates: When codeflash is configured but the binary is not found by +# find_codeflash_binary, the unified hook shows a generic install +# message with "pip install codeflash". +# Expected: Block with reason containing "pip install codeflash". @test "js: configured + NOT installed → install prompt" { add_js_commit create_package_json true - setup_mock_npx false + local restricted_path + restricted_path=$(not_installed_path) - run run_hook false "PATH=$MOCK_BIN:$PATH" + run run_hook false "PATH=$restricted_path" assert_block - assert_reason_contains "npm install --save-dev codeflash" + assert_reason_contains "pip install codeflash" } # Setup: package.json exists but has NO "codeflash" key. Mock npx returns # success (codeflash is installed). One .js commit. -# Validates: When codeflash is installed but not configured, the hook should -# instruct Claude to discover project structure and add the "codeflash" -# config key to package.json with moduleRoot, testsRoot, etc. -# Expected: Block with reason containing "moduleRoot" and "testsRoot" -# (the config fields to be added to package.json). +# Validates: When codeflash is not configured, the unified hook takes the +# NOT CONFIGURED path and shows per-language JS/TS setup instructions +# with the package.json config template. +# Expected: Block with reason containing "moduleRoot" and "testsRoot". @test "js: NOT configured + installed → setup prompt" { add_js_commit create_package_json false @@ -304,13 +303,11 @@ setup() { # Setup: package.json without "codeflash" key. Mock npx fails (not installed). # One .js commit. -# Validates: When both installation and configuration are missing for a JS -# project. The setup message should include an install step -# ("npm install --save-dev codeflash") embedded within the broader -# config setup instructions. -# Expected: Block with reason containing both "moduleRoot" (setup) and -# "npm install --save-dev codeflash" (installation). -@test "js: NOT configured + NOT installed → setup + install prompt" { +# Validates: When configuration is missing, the unified hook takes the +# NOT CONFIGURED path regardless of installation state. Shows +# JS/TS setup instructions with config template. +# Expected: Block with reason containing "moduleRoot" and "testsRoot". +@test "js: NOT configured + NOT installed → setup prompt" { add_js_commit create_package_json false setup_mock_npx false @@ -318,15 +315,14 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block assert_reason_contains "moduleRoot" - assert_reason_contains "npm install --save-dev codeflash" + assert_reason_contains "testsRoot" } # Setup: Configured package.json + mock npx. Commit touches a .ts file # instead of .js. -# Validates: TypeScript files (*.ts) are detected by the git log filter and -# route through the JS project path (since package.json is the -# project config). The hook should treat .ts the same as .js. -# Expected: Block with reason containing "npx codeflash --subagent". +# Validates: TypeScript files (*.ts) are detected by the git log filter. +# The unified hook finds package.json config and runs codeflash. +# Expected: Block with reason containing "codeflash --subagent". @test "js: typescript file triggers JS path" { add_ts_commit "utils.ts" create_package_json true @@ -334,14 +330,14 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" } # Setup: Configured package.json + mock npx. Commit touches a .jsx file. # Validates: JSX files (*.jsx) are also detected by the git log filter -# (-- '*.jsx') and processed via the JS path. Ensures React -# component files trigger optimization. -# Expected: Block with reason containing "npx codeflash --subagent". +# (-- '*.jsx'). The unified hook finds package.json config and +# runs codeflash. +# Expected: Block with reason containing "codeflash --subagent". @test "js: jsx file triggers JS path" { add_js_commit "Component.jsx" create_package_json true @@ -349,7 +345,7 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" } # ═══════════════════════════════════════════════════════════════════════════════ @@ -391,10 +387,8 @@ setup() { } # Setup: Fully configured JS project. No .claude/settings.json exists. -# Validates: Same as the Python auto-allow test, but for JS projects. The -# auto-allow logic is shared (checked at script top before branching -# on project type), but the instructions are appended separately in -# each path. This verifies the JS path also appends them. +# Validates: The unified hook appends auto-allow instructions when +# .claude/settings.json doesn't have codeflash permitted. # Expected: Block reason contains "permissions.allow". @test "js: includes auto-allow instructions when settings.json missing" { add_js_commit @@ -408,7 +402,7 @@ setup() { # Setup: Fully configured JS project. .claude/settings.json has # "Bash(*codeflash*)" in permissions.allow. -# Validates: JS path correctly omits auto-allow instructions when already set. +# Validates: Unified hook correctly omits auto-allow instructions when already set. # Expected: Block reason does NOT contain "permissions.allow". @test "js: omits auto-allow when already configured" { add_js_commit @@ -428,13 +422,11 @@ setup() { # Setup: BOTH pyproject.toml (with [tool.codeflash]) and package.json (with # "codeflash" key) exist in the same directory. Fake venv with # codeflash installed. One .py commit. -# Validates: The detect_project function checks pyproject.toml before -# package.json at each directory level. When both exist, the Python -# path should be chosen. This ensures Python projects with a -# package.json (e.g., for JS tooling) don't accidentally take the -# JS path. -# Expected: Block with "codeflash --subagent" (bare, Python-style) and -# NOT "npx" (which would indicate the JS path). +# Validates: The unified detect_any_config finds both configs. When the project +# is configured, find_codeflash_binary locates the venv binary. +# The hook fires a single `codeflash --subagent` — the CLI handles +# multi-language dispatch. +# Expected: Block with "codeflash --subagent" and "run_in_background". @test "pyproject.toml takes precedence over package.json in same directory" { add_python_commit create_pyproject true @@ -443,16 +435,14 @@ setup() { run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block - # Python path: uses bare codeflash, not npx assert_reason_contains "codeflash --subagent" - assert_reason_not_contains "npx" } # Setup: Only package.json exists (no pyproject.toml). Configured with # "codeflash" key. Mock npx available. One .js commit. -# Validates: When pyproject.toml is absent, detect_project correctly falls -# through to package.json and identifies the project as JS/TS. -# Expected: Block with "npx codeflash --subagent" (JS-style invocation). +# Validates: When pyproject.toml is absent, detect_any_config correctly finds +# package.json. The unified hook runs codeflash --subagent. +# Expected: Block with "codeflash --subagent". @test "detects package.json when no pyproject.toml exists" { add_js_commit # Only package.json, no pyproject.toml @@ -461,5 +451,5 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" } \ No newline at end of file From ef80a3ed248cb2b6187f70e23db146a4da6de45e Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 19 Mar 2026 04:00:10 +0000 Subject: [PATCH 4/4] fix: set CODEFLASH_API_KEY in JS auto-allow tests to bypass OAuth check In CI there's no API key, so has_api_key() returns false and the hook shows the OAuth login prompt instead of reaching the auto-allow message. The Python auto-allow test already passed CODEFLASH_API_KEY=cf-test-key; now the JS tests do too. Co-Authored-By: Claude Opus 4.6 --- tests/test_suggest_optimize.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_suggest_optimize.bats b/tests/test_suggest_optimize.bats index d726e0d..d5760fd 100755 --- a/tests/test_suggest_optimize.bats +++ b/tests/test_suggest_optimize.bats @@ -395,7 +395,7 @@ setup() { create_package_json true setup_mock_npx true - run run_hook false "PATH=$MOCK_BIN:$PATH" + run run_hook false "PATH=$MOCK_BIN:$PATH" "CODEFLASH_API_KEY=cf-test-key" assert_block assert_reason_contains "permissions.allow" } @@ -410,7 +410,7 @@ setup() { setup_mock_npx true create_auto_allow - run run_hook false "PATH=$MOCK_BIN:$PATH" + run run_hook false "PATH=$MOCK_BIN:$PATH" "CODEFLASH_API_KEY=cf-test-key" assert_block assert_reason_not_contains "permissions.allow" }