Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ understood on its own. This file documents the hooks that are currently wired up
- **Failure mode**: blocking — feeds a parsed error summary back to Claude so it
can fix the breakage before yielding

### 3. Test runner (PostToolUse)
- **Script**: `hooks/run-tests.sh`
- **Trigger**: after an `Edit`/`Write`/`MultiEdit` on a `.groovy`/`.java` file under
`modules/` or `plugins/`
- **Action**: maps the edited file to its test class and runs it, e.g.
`modules/nextflow/.../cache/CacheDB.groovy` → `./gradlew :nextflow:test --tests "*CacheDBTest"`
(test files run themselves)
- **Failure mode**: blocking — feeds the test-failure summary back to Claude
- **Scoping**: the script itself decides what is testable (`.groovy`/`.java` under
`modules/` or `plugins/`, mapped to a gradle module). This covers all three edit tools
without gaps, so no declarative `if` is used here. Runs per edit, so the timeout is
generous (300s); narrow the matcher if per-edit test runs are too heavy.

## Enabling / disabling

Hooks are configured in `.claude/settings.json`. Disable one by removing its entry;
Expand Down
56 changes: 56 additions & 0 deletions .claude/hooks/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
#
# Test runner hook for Nextflow development.
#
# After an Edit/Write/MultiEdit on a .groovy/.java file under modules/ or
# plugins/, map it to its test class and run it via gradle. A source file runs
# its corresponding *Test class; a test file runs itself. Block on failure.
#
# Reads the Claude Code hook payload as JSON on stdin (requires `jq`).

set -uo pipefail

command -v jq >/dev/null 2>&1 || exit 0

input=$(cat)
tool_name=$(jq -r '.tool_name // ""' <<<"$input")
file_path=$(jq -r '.tool_input.file_path // ""' <<<"$input")

case "$tool_name" in
Edit|Write|MultiEdit) ;;
*) exit 0 ;;
esac

[[ -n "$file_path" ]] || exit 0
case "$file_path" in *.groovy|*.java) ;; *) exit 0 ;; esac
case "$file_path" in *modules/*|*plugins/*) ;; *) exit 0 ;; esac

# Resolve the gradle module path.
module=""
case "$file_path" in
*plugins/nf-*)
plugin=$(sed -E 's#.*plugins/(nf-[^/]+)/.*#\1#' <<<"$file_path")
module="plugins:${plugin}" ;;
*modules/nextflow/*) module="nextflow" ;;
*modules/nf-commons/*) module="nf-commons" ;;
*modules/nf-httpfs/*) module="nf-httpfs" ;;
*modules/nf-lang/*) module="nf-lang" ;;
*modules/nf-lineage/*) module="nf-lineage" ;;
esac
[[ -n "$module" ]] || exit 0

class=$(basename "$file_path"); class="${class%.*}"
case "$class" in
*Test) test_class="$class" ;;
*) test_class="${class}Test" ;;
esac

if out=$(./gradlew ":${module}:test" --tests "*${test_class}" 2>&1); then
jq -n --arg f "$(basename "$file_path")" \
'{suppressOutput: true, systemMessage: ("✓ Tests passed for " + $f)}'
else
summary=$(printf '%s\n' "$out" | grep -iE 'failed|error|exception|assertion' | tail -n 5)
[[ -n "$summary" ]] || summary=$(printf '%s\n' "$out" | tail -n 15)
jq -n --arg f "$(basename "$file_path")" --arg s "$summary" \
'{decision: "block", reason: ("Tests failed for " + $f + ":\n" + $s)}'
fi
5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-editorconfig.sh",
"timeout": 30,
"async": true
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests.sh",
"timeout": 300
}
]
}
Expand Down
Loading