From 5400f613c1fc34c6cc4ffcacf8a87611111cce9a Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Thu, 19 Mar 2026 18:27:31 -0700 Subject: [PATCH] Fix security issues, reduce duplication, add Java tests, and reconcile agent instructions - Security: disable debug logging to /tmp by default (opt-in via CODEFLASH_HOOK_DEBUG=1) - Security: fix shell-to-Python injection patterns in oauth-login.sh (use sys.argv instead of string interpolation) - Refactor: extract append_auto_allow_msg() and emit_block() helpers to eliminate 10 copies of auto-allow message logic - Refactor: rename misleading CHANGED_COMMITS variable to CHANGED_FILES - Fix: quote paths in cd command strings to handle spaces - Tests: add Java project test coverage (7 new test cases + helpers) - Docs: reconcile contradictory JS/TS setup instructions between hook and agent (both now auto-discover) - Housekeeping: add .idea/ and .claude/settings.local.json to .gitignore --- .gitignore | 2 + agents/optimizer.md | 24 +++--- scripts/oauth-login.sh | 22 ++--- scripts/suggest-optimize.sh | 142 +++++++++++-------------------- tests/helpers/setup.bash | 50 +++++++++++ tests/test_suggest_optimize.bats | 98 +++++++++++++++++++++ 6 files changed, 224 insertions(+), 114 deletions(-) diff --git a/.gitignore b/.gitignore index fd0faaa..b12cce7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__/ *.pyc .orphaned_at +.idea/ +.claude/settings.local.json diff --git a/agents/optimizer.md b/agents/optimizer.md index 72f8b9d..97f0a4b 100644 --- a/agents/optimizer.md +++ b/agents/optimizer.md @@ -227,13 +227,13 @@ When configuration is missing, automatically discover the paths: Do NOT ask the user to choose from a list of options. Use your tools to inspect the actual project structure and determine the correct paths. -3. **Write the configuration**: Append the `[tool.codeflash]` section to the target `pyproject.toml`. Use exactly this format, substituting the user's answers: +3. **Write the configuration**: Append the `[tool.codeflash]` section to the target `pyproject.toml`. Use exactly this format, substituting the discovered paths: ```toml [tool.codeflash] # All paths are relative to this pyproject.toml's directory. -module-root = "" -tests-root = "" +module-root = "" +tests-root = "" ignore-paths = [] formatter-cmds = ["disabled"] ``` @@ -250,21 +250,21 @@ Use the `package.json` discovered in Step 1: - **If `package.json` exists but has no `"codeflash"` key** → add the config to that file. - **If no `package.json` was found** → create one at the git repository root with `npm init -y`, then add the config. -When configuration is missing, interactively set it up: +When configuration is missing, automatically discover the paths: + +1. **Discover module root**: Use Glob and Read to find the relative path to the root of the JavaScript/TypeScript module. Typically `.` for the root directory or `src`. Look for source files, `index.js`/`index.ts`, or directories containing the main entry point referenced in `package.json`. -1. **Ask the user two questions** (use AskUserQuestion or prompt directly): - - **Module root**: "What is the relative path to the root of your JavaScript/TypeScript module?" (e.g. `.` for the root directory, `src`, `src/lib`) - - **Tests folder**: "What is the relative path to your tests folder?" (e.g. `tests`, `test`, `__tests__`, `src/__tests__`) +2. **Discover tests folder**: Use Glob to find the relative path to the tests directory. Look for existing directories named `tests`, `test`, `__tests__`, or folders containing files that start with `test_` or end with `.test.js`/`.test.ts`/`.spec.js`/`.spec.ts`. If no tests directory exists, default to `tests` and create it with `mkdir -p`. -2. **Validate directories**: Check whether the tests folder the user provided exists. If it does **not** exist, create it with `mkdir -p`. +Do NOT ask the user to choose from a list of options. Use your tools to inspect the actual project structure and determine the correct paths. -3. **Write the configuration**: Read the existing `package.json`, parse it as JSON, add a `"codeflash"` key at the root level, and write the file back. Use exactly this structure, substituting the user's answers: +3. **Write the configuration**: Read the existing `package.json`, parse it as JSON, add a `"codeflash"` key at the root level, and write the file back. Use exactly this structure, substituting the discovered paths: ```json { "codeflash": { - "moduleRoot": "", - "testsRoot": "", + "moduleRoot": "", + "testsRoot": "", "formatterCmds": ["disabled"], "ignorePaths": ["dist", "**/node_modules", "**/__tests__"] } @@ -375,6 +375,6 @@ Do not wait for the background task to finish. The user will be notified automat - **No virtual environment**: No `$VIRTUAL_ENV` set and no `.venv`/`venv` directory found — tell the user to create/activate a venv, install codeflash there, and restart Claude Code - **Exit 127 / command not found**: Codeflash not installed in the active venv — ask the user to install it with `pip install codeflash` -- **Not configured**: Interactively ask the user for module root and tests folder, then write the config (Python/JS/TS), or run `codeflash init --yes` (Java) +- **Not configured**: Automatically discover module root and tests folder using Glob and Read, then write the config (Python/JS/TS), or run `codeflash init --yes` (Java) - **No optimizations found**: Normal — not all code can be optimized, report this clearly - **"Attempting to repair broken tests..."**: Normal codeflash behavior, not an error diff --git a/scripts/oauth-login.sh b/scripts/oauth-login.sh index 68e3aed..dd8c732 100755 --- a/scripts/oauth-login.sh +++ b/scripts/oauth-login.sh @@ -130,8 +130,8 @@ if [ "${1:-}" = "--exchange-code" ]; then fi # Read saved state - CODE_VERIFIER=$(python3 -c "import json; print(json.load(open('${STATE_FILE}')).get('code_verifier',''))" 2>/dev/null || true) - REMOTE_REDIRECT=$(python3 -c "import json; print(json.load(open('${STATE_FILE}')).get('remote_redirect_uri',''))" 2>/dev/null || true) + CODE_VERIFIER=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('code_verifier',''))" "$STATE_FILE" 2>/dev/null || true) + REMOTE_REDIRECT=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('remote_redirect_uri',''))" "$STATE_FILE" 2>/dev/null || true) rm -f "$STATE_FILE" @@ -159,8 +159,8 @@ PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.get LOCAL_REDIRECT_URI="http://localhost:${PORT}/callback" REMOTE_REDIRECT_URI="${CFWEBAPP_BASE_URL}/codeflash/auth/callback" -ENCODED_LOCAL_REDIRECT=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${LOCAL_REDIRECT_URI}'))") -ENCODED_REMOTE_REDIRECT=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${REMOTE_REDIRECT_URI}'))") +ENCODED_LOCAL_REDIRECT=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$LOCAL_REDIRECT_URI") +ENCODED_REMOTE_REDIRECT=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$REMOTE_REDIRECT_URI") AUTH_PARAMS="response_type=code&client_id=${CLIENT_ID}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=sha256&state=${STATE}" LOCAL_AUTH_URL="${CFWEBAPP_BASE_URL}/codeflash/auth?${AUTH_PARAMS}&redirect_uri=${ENCODED_LOCAL_REDIRECT}" @@ -171,13 +171,13 @@ if ! can_open_browser; then # Save PKCE state so --exchange-code can complete the flow later HEADLESS_STATE_FILE=$(mktemp /tmp/codeflash-oauth-state-XXXXXX.json) python3 -c " -import json +import json, sys json.dump({ - 'code_verifier': '${CODE_VERIFIER}', - 'remote_redirect_uri': '${REMOTE_REDIRECT_URI}', - 'state': '${STATE}' -}, open('${HEADLESS_STATE_FILE}', 'w')) -" + 'code_verifier': sys.argv[1], + 'remote_redirect_uri': sys.argv[2], + 'state': sys.argv[3] +}, open(sys.argv[4], 'w')) +" "$CODE_VERIFIER" "$REMOTE_REDIRECT_URI" "$STATE" "$HEADLESS_STATE_FILE" # Output JSON for Claude to parse — this is the ONLY stdout in headless mode printf '{"headless":true,"url":"%s","state_file":"%s"}\n' "$REMOTE_AUTH_URL" "$HEADLESS_STATE_FILE" exit 2 @@ -388,7 +388,7 @@ if [ ! -s "$RESULT_FILE" ]; then fi # --- Parse callback result --- -AUTH_CODE=$(python3 -c "import json; print(json.load(open('${RESULT_FILE}')).get('code',''))" 2>/dev/null || true) +AUTH_CODE=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('code',''))" "$RESULT_FILE" 2>/dev/null || true) if [ -z "$AUTH_CODE" ]; then kill "$SERVER_PID" 2>/dev/null || true diff --git a/scripts/suggest-optimize.sh b/scripts/suggest-optimize.sh index fa12c15..0d64a10 100755 --- a/scripts/suggest-optimize.sh +++ b/scripts/suggest-optimize.sh @@ -5,9 +5,12 @@ set -euo pipefail -LOGFILE="/tmp/codeflash-hook-debug.log" -exec 2>>"$LOGFILE" -set -x +# Debug logging — only enabled when CODEFLASH_HOOK_DEBUG=1 +if [ "${CODEFLASH_HOOK_DEBUG:-}" = "1" ]; then + LOGFILE="${CODEFLASH_HOOK_LOGFILE:-/tmp/codeflash-hook-debug.log}" + exec 2>>"$LOGFILE" + set -x +fi # Read stdin (Stop hook pipes context as JSON via stdin) INPUT=$(cat) @@ -72,18 +75,18 @@ if [ -z "$SESSION_START" ] || [ "$SESSION_START" = "0" ]; then 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 +CHANGED_FILES=$(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_FILES" ]; then exit 0 fi # Determine which language families actually had changes HAS_PYTHON_CHANGES="false" HAS_JS_CHANGES="false" -if echo "$CHANGED_COMMITS" | grep -qE '\.py$'; then +if echo "$CHANGED_FILES" | grep -qE '\.py$'; then HAS_PYTHON_CHANGES="true" fi -if echo "$CHANGED_COMMITS" | grep -qE '\.(js|ts|jsx|tsx)$'; then +if echo "$CHANGED_FILES" | grep -qE '\.(js|ts|jsx|tsx)$'; then HAS_JS_CHANGES="true" fi @@ -127,6 +130,24 @@ if ! has_api_key; then - Run \`${OAUTH_SCRIPT}\` to log in to Codeflash. If it exits with code 0, the key is saved. If it exits with code 2 (headless environment), parse the JSON output for the \`url\` and \`state_file\`, ask the user to visit the URL and provide the authorization code, then run \`${OAUTH_SCRIPT} --exchange-code \` to complete the login." fi +# --- Helper: append auto-allow instructions if not already configured --- +# Usage: append_auto_allow_msg MSG_VAR_NAME +# Appends the auto-allow setup instructions to the variable named MSG_VAR_NAME +# if codeflash is not already auto-allowed in .claude/settings.json. +append_auto_allow_msg() { + if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then + local current="${!1}" + printf -v "$1" '%s\n\nThen, add `Bash(*codeflash*)` to the `permissions.allow` array in `%s` (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.' "$current" "$SETTINGS_JSON" + fi +} + +# --- Helper: emit a block decision as JSON and exit --- +# Usage: emit_block "$MESSAGE" +emit_block() { + jq -nc --arg reason "$1" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' + exit 0 +} + # Walk from $PWD upward to $REPO_ROOT looking for project config. # Sets: PROJECT_TYPE, PROJECT_DIR, PROJECT_CONFIG_PATH, PROJECT_CONFIGURED detect_project() { @@ -229,15 +250,8 @@ Once determined:${INSTALL_STEP} 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, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg SETUP_MSG + emit_block "$SETUP_MSG" fi # Configured but not installed @@ -250,35 +264,21 @@ Install codeflash to optimize the JavaScript/TypeScript code for performance by 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, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg INSTALL_MSG + emit_block "$INSTALL_MSG" 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" + RUN_CMD="cd \"$PROJECT_DIR\" && npx codeflash --subagent" else RUN_CMD="npx codeflash --subagent" 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." - fi - - jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg MESSAGE + emit_block "$MESSAGE" fi # --- Java project path ----------------------------------------------------- @@ -306,14 +306,8 @@ Install codeflash to optimize the Java code for performance by running: Then run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - 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, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg INSTALL_MSG + emit_block "$INSTALL_MSG" fi # Not configured @@ -322,33 +316,21 @@ Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTING 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, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg SETUP_MSG + emit_block "$SETUP_MSG" fi # Everything ready — build codeflash command for Java if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then - RUN_CMD="cd $PROJECT_DIR && $CODEFLASH_BIN --subagent" + 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." - 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 - - jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg MESSAGE + emit_block "$MESSAGE" fi # --- Python project path --------------------------------------------------- @@ -414,8 +396,7 @@ ${LOGIN_STEP} ${PYPROJECT_SETUP} ${SETUP_PERMISSIONS_STEP}" - jq -nc --arg reason "$VENV_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 + emit_block "$VENV_MSG" fi CODEFLASH_BIN="${VIRTUAL_ENV}/bin/codeflash" @@ -463,15 +444,8 @@ formatter-cmds = [\"disabled\"] Once the pyproject.toml configuration is written, 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 - 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, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg SETUP_MSG + emit_block "$SETUP_MSG" fi # Codeflash is configured but not installed in the venv @@ -484,15 +458,8 @@ Install codeflash to optimize the Python code for performance by running: 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, "systemMessage": $reason}' - exit 0 + append_auto_allow_msg INSTALL_MSG + emit_block "$INSTALL_MSG" fi # Check for API key before running codeflash @@ -503,24 +470,17 @@ Run \`${OAUTH_SCRIPT}\` to log in to Codeflash. If it exits with code 0, the key After login, run \`codeflash --subagent\` as a background Bash task (run_in_background: true) to optimize the code." - jq -nc --arg reason "$LOGIN_MSG" '{"decision": "block", "reason": $reason}' - exit 0 + emit_block "$LOGIN_MSG" 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" + 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 - -jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' +append_auto_allow_msg MESSAGE +emit_block "$MESSAGE" diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index 4302c5c..fa7503c 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -93,6 +93,17 @@ add_irrelevant_commit() { git -C "$REPO" commit -m "add $file" >/dev/null 2>&1 } +add_java_commit() { + local file="${1:-Main.java}" + mkdir -p "$REPO/$(dirname "$file")" + echo 'public class Main { public static void main(String[] args) {} }' > "$REPO/$file" + git -C "$REPO" add -A >/dev/null 2>&1 + local ts + ts=$(future_timestamp) + GIT_COMMITTER_DATE="@$ts" GIT_AUTHOR_DATE="@$ts" \ + git -C "$REPO" commit -m "add $file" >/dev/null 2>&1 +} + # --------------------------------------------------------------------------- # Project configuration helpers # --------------------------------------------------------------------------- @@ -173,6 +184,45 @@ EOF git -C "$REPO" commit -m "add package.json" --allow-empty >/dev/null 2>&1 } +# Create codeflash.toml for Java projects. configured=true adds [tool.codeflash]. +create_codeflash_toml() { + local configured="${1:-true}" + if [ "$configured" = "true" ]; then + cat > "$REPO/codeflash.toml" << 'EOF' +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +ignore-paths = [] +EOF + else + cat > "$REPO/codeflash.toml" << 'EOF' +# Codeflash configuration +EOF + fi + git -C "$REPO" add -A >/dev/null 2>&1 + git -C "$REPO" commit -m "add codeflash.toml" --allow-empty >/dev/null 2>&1 +} + +# Create a mock codeflash binary in MOCK_BIN (for Java projects that use PATH). +# Usage: setup_mock_codeflash_bin [installed=true] +# When installed=false, no codeflash binary is created (simulates not installed). +setup_mock_codeflash_bin() { + local installed="${1:-true}" + mkdir -p "$MOCK_BIN" + + if [ "$installed" = "true" ]; then + cat > "$MOCK_BIN/codeflash" << 'MOCK' +#!/bin/bash +echo "codeflash 0.1.0" +exit 0 +MOCK + chmod +x "$MOCK_BIN/codeflash" + else + # Ensure no codeflash binary exists so `command -v` fails + rm -f "$MOCK_BIN/codeflash" + fi +} + # Create .claude/settings.json with Bash(*codeflash*) auto-allowed create_auto_allow() { mkdir -p "$REPO/.claude" diff --git a/tests/test_suggest_optimize.bats b/tests/test_suggest_optimize.bats index 2e74e80..0875867 100755 --- a/tests/test_suggest_optimize.bats +++ b/tests/test_suggest_optimize.bats @@ -352,6 +352,104 @@ setup() { assert_reason_contains "npx codeflash --subagent" } +# ═══════════════════════════════════════════════════════════════════════════════ +# Java projects +# ═══════════════════════════════════════════════════════════════════════════════ + +# Setup: codeflash.toml with [tool.codeflash] section. Mock codeflash binary +# available on PATH. One .java file committed after session start. +# Validates: The Java "happy path" — everything is set up, codeflash should run. +# The hook instructs Claude to execute `codeflash --subagent` as a +# background task. +# Expected: Block with reason containing "codeflash --subagent" and +# "run_in_background". +@test "java: configured + codeflash installed → run codeflash" { + add_java_commit + create_codeflash_toml true + setup_mock_codeflash_bin true + + run run_hook false "PATH=$MOCK_BIN:$PATH" + assert_block + assert_reason_contains "codeflash --subagent" + assert_reason_contains "run_in_background" +} + +# Setup: codeflash.toml with [tool.codeflash]. No codeflash binary +# available on PATH (mock bin has no codeflash, system PATH excluded). +# Validates: When codeflash is configured but not installed, the hook should +# prompt the user to install it before optimization can run. +# Expected: Block with reason containing "pip install codeflash". +@test "java: configured + codeflash NOT installed → install prompt" { + add_java_commit + create_codeflash_toml true + setup_mock_codeflash_bin false + + # Use restricted PATH to exclude any system-installed codeflash/uv + run run_hook false "PATH=$MOCK_BIN:/usr/bin:/bin" + assert_block + assert_reason_contains "pip install codeflash" +} + +# Setup: codeflash.toml exists but has NO [tool.codeflash] section. Mock +# codeflash binary available on PATH. +# Validates: When codeflash is installed but not configured for Java, the hook +# should instruct Claude to run `codeflash init --yes` to auto-detect +# the project structure. +# Expected: Block with reason containing "init --yes". +@test "java: NOT configured + codeflash installed → setup prompt" { + add_java_commit + create_codeflash_toml false + setup_mock_codeflash_bin true + + run run_hook false "PATH=$MOCK_BIN:$PATH" + assert_block + assert_reason_contains "init --yes" +} + +# Setup: codeflash.toml without [tool.codeflash]. No codeflash binary +# available on PATH (system PATH excluded). +# Validates: When both installation and configuration are missing for a Java +# project, the install prompt takes priority (checked first in code). +# Expected: Block with reason containing "pip install codeflash". +@test "java: NOT configured + NOT installed → install prompt" { + add_java_commit + create_codeflash_toml false + setup_mock_codeflash_bin false + + # Use restricted PATH to exclude any system-installed codeflash/uv + run run_hook false "PATH=$MOCK_BIN:/usr/bin:/bin" + assert_block + assert_reason_contains "pip install codeflash" +} + +# Setup: Fully configured Java project. No .claude/settings.json exists. +# Validates: Java path also appends auto-allow instructions when missing. +# Expected: Block reason contains "permissions.allow". +@test "java: includes auto-allow instructions when settings.json missing" { + add_java_commit + create_codeflash_toml true + setup_mock_codeflash_bin true + + run run_hook false "PATH=$MOCK_BIN:$PATH" + assert_block + assert_reason_contains "permissions.allow" +} + +# Setup: Fully configured Java project. .claude/settings.json has +# "Bash(*codeflash*)" in permissions.allow. +# Validates: Java path correctly omits auto-allow instructions when already set. +# Expected: Block reason does NOT contain "permissions.allow". +@test "java: omits auto-allow when already configured" { + add_java_commit + create_codeflash_toml true + setup_mock_codeflash_bin true + create_auto_allow + + run run_hook false "PATH=$MOCK_BIN:$PATH" + assert_block + assert_reason_not_contains "permissions.allow" +} + # ═══════════════════════════════════════════════════════════════════════════════ # Permissions — auto-allow instructions # ═══════════════════════════════════════════════════════════════════════════════