diff --git a/README.md b/README.md index 7a0f85f..2c43aac 100644 --- a/README.md +++ b/README.md @@ -81,18 +81,16 @@ codeflash-cc-plugin/ ├── .claude-plugin/ │ ├── marketplace.json # Marketplace manifest │ └── plugin.json # Plugin manifest -├── agents/ -│ └── optimizer.md # Background optimization agent -├── commands/ -│ └── setup.md # /setup command for auto-allow permissions ├── hooks/ │ └── hooks.json # Stop hook for commit detection ├── scripts/ │ ├── find-venv.sh # Shared helper: find and activate a Python venv │ └── suggest-optimize.sh # Detects Python/Java/JS/TS changes, suggests /optimize ├── skills/ -│ └── optimize/ -│ └── SKILL.md # /optimize slash command +│ ├── optimize/ +│ │ └── SKILL.md # /optimize slash command +│ └── setup/ +│ └── SKILL.md # /setup command for installation and configuration └── README.md ``` diff --git a/scripts/oauth-login.sh b/scripts/oauth-login.sh deleted file mode 100755 index 68e3aed..0000000 --- a/scripts/oauth-login.sh +++ /dev/null @@ -1,405 +0,0 @@ -#!/usr/bin/env bash -# OAuth PKCE login flow for Codeflash. -# Opens browser for authentication, exchanges code for API key, -# and saves it to the user's shell RC file. -# -# Usage: -# ./oauth-login.sh # Full browser flow -# ./oauth-login.sh --exchange-code STATE_FILE CODE # Complete headless flow -# -# Exit codes: -# 0 = success (API key saved) -# 1 = error -# 2 = headless mode (remote URL printed to stdout, state saved to temp file) - -set -euo pipefail - -CFWEBAPP_BASE_URL="https://app.codeflash.ai" -TOKEN_URL="${CFWEBAPP_BASE_URL}/codeflash/auth/oauth/token" -CLIENT_ID="cf-cli-app" -TIMEOUT=180 - -# --- Detect if a browser can be launched (matches codeflash's should_attempt_browser_launch) --- -can_open_browser() { - # CI/CD or non-interactive environments - if [ -n "${CI:-}" ] || [ "${DEBIAN_FRONTEND:-}" = "noninteractive" ]; then - return 1 - fi - - # Text-only browsers - local browser_env="${BROWSER:-}" - case "$browser_env" in - www-browser|lynx|links|w3m|elinks|links2) return 1 ;; - esac - - local is_ssh="false" - if [ -n "${SSH_CONNECTION:-}" ]; then - is_ssh="true" - fi - - # Linux: require a display server - if [ "$(uname -s)" = "Linux" ]; then - if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] && [ -z "${MIR_SOCKET:-}" ]; then - return 1 - fi - fi - - # SSH on non-Linux (e.g. macOS remote) — no browser - if [ "$is_ssh" = "true" ] && [ "$(uname -s)" != "Linux" ]; then - return 1 - fi - - return 0 -} - -# --- Save API key to shell RC (matches codeflash's shell_utils.py logic) --- -save_api_key() { - local api_key="$1" - - if [ "${OS:-}" = "Windows_NT" ] || [[ "$(uname -s 2>/dev/null)" == MINGW* ]]; then - # Windows: use dedicated codeflash env files (same as codeflash CLI) - if [ -n "${PSMODULEPATH:-}" ]; then - RC_FILE="$HOME/codeflash_env.ps1" - EXPORT_LINE="\$env:CODEFLASH_API_KEY = \"${api_key}\"" - REMOVE_PATTERN='^\$env:CODEFLASH_API_KEY' - else - RC_FILE="$HOME/codeflash_env.bat" - EXPORT_LINE="set CODEFLASH_API_KEY=\"${api_key}\"" - REMOVE_PATTERN='^set CODEFLASH_API_KEY=' - fi - else - # Unix: use shell RC file (same mapping as codeflash CLI) - SHELL_NAME=$(basename "${SHELL:-/bin/bash}") - case "$SHELL_NAME" in - zsh) RC_FILE="$HOME/.zshrc" ;; - ksh) RC_FILE="$HOME/.kshrc" ;; - csh|tcsh) RC_FILE="$HOME/.cshrc" ;; - dash) RC_FILE="$HOME/.profile" ;; - *) RC_FILE="$HOME/.bashrc" ;; - esac - EXPORT_LINE="export CODEFLASH_API_KEY=\"${api_key}\"" - REMOVE_PATTERN='^export CODEFLASH_API_KEY=' - fi - - # Remove any existing CODEFLASH_API_KEY lines and append the new one - if [ -f "$RC_FILE" ]; then - CLEANED=$(grep -v "$REMOVE_PATTERN" "$RC_FILE" || true) - printf '%s\n' "$CLEANED" > "$RC_FILE" - fi - printf '%s\n' "$EXPORT_LINE" >> "$RC_FILE" - - # Also export for the current session - export CODEFLASH_API_KEY="$api_key" -} - -# --- Exchange code for token and save --- -exchange_and_save() { - local auth_code="$1" - local code_verifier="$2" - local redirect_uri="$3" - - TOKEN_RESPONSE=$(curl -s -X POST "$TOKEN_URL" \ - -H "Content-Type: application/json" \ - -d "{ - \"grant_type\": \"authorization_code\", - \"code\": \"${auth_code}\", - \"code_verifier\": \"${code_verifier}\", - \"redirect_uri\": \"${redirect_uri}\", - \"client_id\": \"${CLIENT_ID}\" - }") - - API_KEY=$(printf '%s' "$TOKEN_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) - - if [ -z "$API_KEY" ] || [[ ! "$API_KEY" == cf-* ]]; then - exit 1 - fi - - save_api_key "$API_KEY" -} - -# =================================================================== -# Mode: --exchange-code STATE_FILE CODE -# Complete a headless flow using a previously saved PKCE state file. -# =================================================================== -if [ "${1:-}" = "--exchange-code" ]; then - STATE_FILE="${2:-}" - MANUAL_CODE="${3:-}" - - if [ -z "$STATE_FILE" ] || [ -z "$MANUAL_CODE" ] || [ ! -f "$STATE_FILE" ]; then - exit 1 - 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) - - rm -f "$STATE_FILE" - - if [ -z "$CODE_VERIFIER" ] || [ -z "$REMOTE_REDIRECT" ]; then - exit 1 - fi - - exchange_and_save "$MANUAL_CODE" "$CODE_VERIFIER" "$REMOTE_REDIRECT" - exit 0 -fi - -# =================================================================== -# Mode: Full OAuth flow (default) -# =================================================================== - -# --- PKCE pair --- -CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=/+\n' | head -c 64) -CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') - -# --- State --- -STATE=$(openssl rand -hex 16) - -# --- Find a free port --- -PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()") - -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}'))") - -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}" -REMOTE_AUTH_URL="${CFWEBAPP_BASE_URL}/codeflash/auth?${AUTH_PARAMS}&redirect_uri=${ENCODED_REMOTE_REDIRECT}" - -# --- Headless detection --- -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 -json.dump({ - 'code_verifier': '${CODE_VERIFIER}', - 'remote_redirect_uri': '${REMOTE_REDIRECT_URI}', - 'state': '${STATE}' -}, open('${HEADLESS_STATE_FILE}', 'w')) -" - # 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 -fi - -# --- Temp file for callback result --- -RESULT_FILE=$(mktemp /tmp/codeflash-oauth-XXXXXX.json) -trap 'rm -f "$RESULT_FILE"' EXIT - -# --- Start local callback server with Codeflash-styled pages --- -export PORT STATE RESULT_FILE TIMEOUT -python3 - "$PORT" "$STATE" "$RESULT_FILE" << 'PYEOF' & -import http.server, urllib.parse, json, sys, threading - -port = int(sys.argv[1]) -state = sys.argv[2] -result_file = sys.argv[3] - -STYLE = ( - ":root{" - "--bg:hsl(0,0%,99%);--fg:hsl(222.2,84%,4.9%);--card:hsl(0,0%,100%);" - "--card-fg:hsl(222.2,84%,4.9%);--primary:hsl(38,100%,63%);" - "--muted-fg:hsl(41,8%,46%);--border:hsl(41,30%,90%);" - "--destructive:hsl(0,84.2%,60.2%);--destructive-fg:#fff;" - "--success:hsl(142,76%,36%)}" - "html.dark{" - "--bg:hsl(0,6%,5%);--fg:#fff;--card:hsl(0,3%,11%);" - "--card-fg:#fff;--primary:hsl(38,100%,63%);" - "--muted-fg:hsl(48,20%,65%);--border:hsl(48,20%,25%);" - "--destructive:hsl(0,62.8%,30.6%)}" - "*{margin:0;padding:0;box-sizing:border-box}" - "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;" - "background:var(--bg);color:var(--fg);min-height:100vh;" - "display:flex;align-items:center;justify-content:center;padding:20px;position:relative}" - "body::before{content:'';position:fixed;inset:0;" - "background:linear-gradient(to bottom,hsl(38,100%,63%,.1),hsl(38,100%,63%,.05),transparent);" - "pointer-events:none;z-index:0}" - "body::after{content:'';position:fixed;inset:0;" - "background-image:linear-gradient(to right,rgba(128,128,128,.03) 1px,transparent 1px)," - "linear-gradient(to bottom,rgba(128,128,128,.03) 1px,transparent 1px);" - "background-size:24px 24px;pointer-events:none;z-index:0}" - ".ctr{max-width:420px;width:100%;position:relative;z-index:1}" - ".logo-ctr{display:flex;justify-content:center;margin-bottom:48px}" - ".logo{height:40px;width:auto}" - ".ll{display:block}.ld{display:none}" - "html.dark .ll{display:none}html.dark .ld{display:block}" - ".card{background:var(--card);color:var(--card-fg);border:1px solid var(--border);" - "border-radius:16px;box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);" - "padding:48px;animation:fadeIn .3s ease-out forwards}" - "@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}" - ".ic{width:48px;height:48px;background:hsl(38,100%,63%,.1);border-radius:12px;" - "display:flex;align-items:center;justify-content:center;margin:0 auto 24px}" - ".spinner{width:24px;height:24px;border:2px solid var(--border);" - "border-top-color:var(--primary);border-radius:50%;animation:spin .8s linear infinite}" - "@keyframes spin{to{transform:rotate(360deg)}}" - ".si{width:64px;height:64px;background:hsl(142,76%,36%,.1);border-radius:12px;" - "display:flex;align-items:center;justify-content:center;margin:0 auto 24px}" - ".sc{width:32px;height:32px;stroke:hsl(142,76%,36%)}" - "h1{font-size:24px;font-weight:600;margin:0 0 12px;color:var(--card-fg);text-align:center}" - "p{color:var(--muted-fg);margin:0;font-size:14px;line-height:1.5;text-align:center}" - ".eb{background:var(--destructive);color:var(--destructive-fg);" - "padding:14px 18px;border-radius:8px;margin-top:24px;font-size:14px;line-height:1.5;text-align:center}" - "@media(max-width:480px){.card{padding:32px 24px}h1{font-size:20px}.logo{height:32px}}" -) - -LOGO = ( - '
' - '' - '' - '
' -) - -# Pre-built static HTML fragments (no user data) -SUCCESS_FRAG = ( - '
' - '' - '

Success!

' - '

Authentication completed. You can close this window and return to your terminal.

' -) - -ERR_ICON_FRAG = ( - '
' - '' - '' - '' - '' - '

Authentication Failed

' -) - - -def loading_page(): - # Inline the static fragments as JS string constants so the polling - # script only inserts pre-defined trusted HTML, never user data. - success_js = json.dumps(SUCCESS_FRAG) - err_icon_js = json.dumps(ERR_ICON_FRAG) - return ( - '' - '' - 'CodeFlash Authentication' - f'' - '' - '' - f'
{LOGO}' - '
' - '
' - '

Authenticating

' - '

Please wait while we verify your credentials...

' - '
' - '' - ) - - -class H(http.server.BaseHTTPRequestHandler): - server_version = "CFHTTP" - - def do_GET(self): - p = urllib.parse.urlparse(self.path) - - if p.path == "/status": - self.send_response(200) - self.send_header("Content-type", "application/json") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - body = json.dumps({ - "success": self.server.token_error is None and self.server.auth_code is not None, - "error": self.server.token_error, - }) - self.wfile.write(body.encode()) - return - - if p.path != "/callback": - self.send_response(404) - self.end_headers() - return - - params = urllib.parse.parse_qs(p.query) - code = params.get("code", [None])[0] - recv_state = params.get("state", [None])[0] - error = params.get("error", [None])[0] - - if error or not code or recv_state != state: - self.server.token_error = error or "state_mismatch" - else: - self.server.auth_code = code - with open(result_file, "w") as f: - json.dump({"code": code}, f) - - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write(loading_page().encode()) - - def log_message(self, *a): - pass - - -httpd = http.server.HTTPServer(("localhost", port), H) -httpd.auth_code = None -httpd.token_error = None -httpd.serve_forever() -PYEOF -SERVER_PID=$! - -# --- Open browser (macOS, Linux, WSL) --- -if [[ "$(uname)" == "Darwin" ]]; then - open "$LOCAL_AUTH_URL" 2>/dev/null || true -elif command -v wslview >/dev/null 2>&1; then - wslview "$LOCAL_AUTH_URL" 2>/dev/null || true -elif command -v xdg-open >/dev/null 2>&1; then - xdg-open "$LOCAL_AUTH_URL" 2>/dev/null || true -elif command -v cmd.exe >/dev/null 2>&1; then - cmd.exe /c start "" "$LOCAL_AUTH_URL" 2>/dev/null || true -fi - -# --- Wait for callback --- -WAITED=0 -while [ ! -s "$RESULT_FILE" ] && [ "$WAITED" -lt "$TIMEOUT" ]; do - sleep 1 - WAITED=$((WAITED + 1)) - if ! kill -0 "$SERVER_PID" 2>/dev/null; then - break - fi -done - -if [ ! -s "$RESULT_FILE" ]; then - kill "$SERVER_PID" 2>/dev/null || true - wait "$SERVER_PID" 2>/dev/null || true - exit 1 -fi - -# --- Parse callback result --- -AUTH_CODE=$(python3 -c "import json; print(json.load(open('${RESULT_FILE}')).get('code',''))" 2>/dev/null || true) - -if [ -z "$AUTH_CODE" ]; then - kill "$SERVER_PID" 2>/dev/null || true - wait "$SERVER_PID" 2>/dev/null || true - exit 1 -fi - -# --- Exchange code for token --- -exchange_and_save "$AUTH_CODE" "$CODE_VERIFIER" "$LOCAL_REDIRECT_URI" - -# Give the browser a moment to poll /status and see success, then shut down -sleep 2 -kill "$SERVER_PID" 2>/dev/null || true -wait "$SERVER_PID" 2>/dev/null || true \ No newline at end of file diff --git a/skills/optimize/SKILL.md b/skills/optimize/SKILL.md index 10b808b..8b4c646 100644 --- a/skills/optimize/SKILL.md +++ b/skills/optimize/SKILL.md @@ -1,6 +1,6 @@ --- name: optimize -description: Optimize Python, Java, JavaScript, or TypeScript code for performance using Codeflash +description: Optimize code for performance using Codeflash user-invocable: true argument-hint: "[--file path] [--function name]" allowed-tools: ["Bash"] diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index bc007b1..45f7a94 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -83,6 +83,17 @@ add_ts_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 {}" > "$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 +} + add_irrelevant_commit() { local file="${1:-data.txt}" echo "some data" > "$REPO/$file" diff --git a/tests/test_suggest_optimize.bats b/tests/test_suggest_optimize.bats index ccde695..3e9b2b7 100755 --- a/tests/test_suggest_optimize.bats +++ b/tests/test_suggest_optimize.bats @@ -192,81 +192,76 @@ setup() { # Setup: pyproject.toml with [tool.codeflash]. Fake venv exists but does NOT # contain a codeflash binary. VIRTUAL_ENV set. -# Validates: When codeflash is configured but not installed in the venv, the -# hook should prompt the user to install it before optimization can run. -# Expected: Block with reason containing "pip install codeflash". -@test "python: configured + codeflash NOT installed → install prompt" { +# Validates: The simplified hook delegates all setup/install logic to the +# codeflash:optimize skill. Regardless of installation state, the +# hook should block and point to the skill. +# Expected: Block with reason containing "codeflash:optimize". +@test "python: configured + codeflash NOT installed → delegates to skill" { add_python_commit create_pyproject true create_fake_venv "$REPO/.venv" false run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block - assert_reason_contains "pip install codeflash" + assert_reason_contains "codeflash:optimize" } # Setup: pyproject.toml exists but has NO [tool.codeflash] section. Fake venv # with codeflash binary installed. VIRTUAL_ENV set. -# Validates: When codeflash is installed but not configured, the hook should -# instruct Claude to discover the project structure (module root, -# tests folder) and write the [tool.codeflash] config section. -# Expected: Block with reason containing "[tool.codeflash]" and "module-root" -# (the config fields to be written). -@test "python: NOT configured + codeflash installed → setup prompt" { +# Validates: The hook no longer checks configuration state — it delegates to +# the skill which handles setup fallback internally. +# Expected: Block with reason containing "codeflash:optimize". +@test "python: NOT configured + codeflash installed → delegates to skill" { add_python_commit create_pyproject false create_fake_venv "$REPO/.venv" run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block - assert_reason_contains "[tool.codeflash]" - assert_reason_contains "module-root" + assert_reason_contains "codeflash:optimize" } # 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: Even when both installation and configuration are missing, the hook +# delegates to the skill (which handles setup fallback). +# Expected: Block with reason containing "codeflash:optimize". +@test "python: NOT configured + NOT installed → delegates to skill" { add_python_commit create_pyproject false create_fake_venv "$REPO/.venv" false run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block - assert_reason_contains "[tool.codeflash]" - assert_reason_contains "install codeflash" + assert_reason_contains "codeflash:optimize" } # 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 -# reaches the no-venv code path. Currently the script exits non-zero -# due to an unset SETUP_PERMISSIONS_STEP variable (known issue). -# Expected: Exit non-zero (script bug: unset variable with set -u). -@test "python: no venv + configured → exits non-zero (known script bug)" { +# Validates: The simplified hook no longer inspects venv state — it just detects +# Python file changes and delegates to the skill. +# Expected: Block with reason containing "codeflash:optimize". +@test "python: no venv + configured → delegates to skill" { add_python_commit create_pyproject true # No venv created, no VIRTUAL_ENV set run run_hook false - [ "$status" -ne 0 ] + assert_block + assert_reason_contains "codeflash:optimize" } # Setup: pyproject.toml WITHOUT [tool.codeflash]. No venv anywhere. # VIRTUAL_ENV not set. -# Validates: Same no-venv code path. Currently the script exits non-zero -# due to an unset SETUP_PERMISSIONS_STEP variable (known issue). -# Expected: Exit non-zero (script bug: unset variable with set -u). -@test "python: no venv + NOT configured → exits non-zero (known script bug)" { +# Validates: Same as above — hook delegates regardless of project state. +# Expected: Block with reason containing "codeflash:optimize". +@test "python: no venv + NOT configured → delegates to skill" { add_python_commit create_pyproject false run run_hook false - [ "$status" -ne 0 ] + assert_block + assert_reason_contains "codeflash:optimize" } # Setup: pyproject.toml with [tool.codeflash]. Fake venv at $REPO/.venv with @@ -311,55 +306,47 @@ setup() { # 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". -@test "js: configured + NOT installed → install prompt" { +# Validates: The simplified hook delegates all setup/install logic to the +# codeflash:optimize skill regardless of installation state. +# Expected: Block with reason containing "codeflash:optimize". +@test "js: configured + NOT installed → delegates to skill" { add_js_commit create_package_json true setup_mock_npx false run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npm install --save-dev codeflash" + assert_reason_contains "codeflash:optimize" } # 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). -@test "js: NOT configured + installed → setup prompt" { +# Validates: The hook no longer checks configuration state — it delegates to +# the skill which handles setup fallback internally. +# Expected: Block with reason containing "codeflash:optimize". +@test "js: NOT configured + installed → delegates to skill" { add_js_commit create_package_json false setup_mock_npx true run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "moduleRoot" - assert_reason_contains "testsRoot" + assert_reason_contains "codeflash:optimize" } # 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: Even when both installation and configuration are missing, the hook +# delegates to the skill (which handles setup fallback). +# Expected: Block with reason containing "codeflash:optimize". +@test "js: NOT configured + NOT installed → delegates to skill" { add_js_commit create_package_json false setup_mock_npx false 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 "codeflash:optimize" } # Setup: Configured package.json + mock npx. Commit touches a .ts file @@ -393,6 +380,50 @@ setup() { assert_reason_contains "codeflash:optimize" } +# ═══════════════════════════════════════════════════════════════════════════════ +# Java projects +# ═══════════════════════════════════════════════════════════════════════════════ + +# Setup: One .java file committed after session start. +# Validates: The hook detects Java file changes and produces a block decision +# pointing to the codeflash:optimize skill. +# Expected: Block with reason containing "codeflash:optimize" and "Java". +@test "java: .java commit triggers Java path" { + add_java_commit + + run run_hook false + assert_block + assert_reason_contains "codeflash:optimize" + assert_reason_contains "Java" +} + +# Setup: One .java file committed. Run the hook twice. +# Validates: Dedup logic works for Java commits (same as Python/JS). +# Expected: First run blocks; second run exits silently. +@test "java: dedup prevents double trigger" { + add_java_commit + + run run_hook false + assert_block + + run run_hook false + assert_no_block +} + +# Setup: One .java file committed. Verify the message does NOT mention JS/TS +# or Python — it should be Java-specific. +# Validates: Java commits route through the Java branch, not JS or Python. +# Expected: Block reason contains "Java", not "JS/TS" or "Python". +@test "java: message is Java-specific, not JS or Python" { + add_java_commit + + run run_hook false + assert_block + assert_reason_contains "Java" + assert_reason_not_contains "JS/TS" + assert_reason_not_contains "Python" +} + # ═══════════════════════════════════════════════════════════════════════════════ # Permissions — auto-allow instructions # ═══════════════════════════════════════════════════════════════════════════════