Skip to content

Commit 578641e

Browse files
committed
feat(hooks): add test-runner PostToolUse hook
After editing a .groovy/.java file under modules/ or plugins/, map it to its test class and run it via gradle (e.g. CacheDB.groovy -> ":nextflow:test --tests *CacheDBTest"); test files run themselves. On failure, block and feed the failure summary back to Claude, falling back to the tail of the output when no failure lines match. Written in Bash; parses the payload with jq. Wired as a PostToolUse hook because it keys off tool_input.file_path, which is only present on Edit/Write/MultiEdit events. Scoping lives in the script (covers all three edit tools and both extensions with no gaps), so no declarative `if` is used here. Stacked on hooks-2-build-check. Signed-off-by: Edmund Miller <edmund.miller@seqera.io>
1 parent 8eef39e commit 578641e

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

.claude/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ understood on its own. This file documents the hooks that are currently wired up
3030
- **Failure mode**: blocking — feeds a parsed error summary back to Claude so it
3131
can fix the breakage before yielding
3232

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

3548
Hooks are configured in `.claude/settings.json`. Disable one by removing its entry;

.claude/hooks/run-tests.sh

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Test runner hook for Nextflow development.
4+
#
5+
# After an Edit/Write/MultiEdit on a .groovy/.java file under modules/ or
6+
# plugins/, map it to its test class and run it via gradle. A source file runs
7+
# its corresponding *Test class; a test file runs itself. Block on failure.
8+
#
9+
# Reads the Claude Code hook payload as JSON on stdin (requires `jq`).
10+
11+
set -uo pipefail
12+
13+
command -v jq >/dev/null 2>&1 || exit 0
14+
15+
input=$(cat)
16+
tool_name=$(jq -r '.tool_name // ""' <<<"$input")
17+
file_path=$(jq -r '.tool_input.file_path // ""' <<<"$input")
18+
19+
case "$tool_name" in
20+
Edit|Write|MultiEdit) ;;
21+
*) exit 0 ;;
22+
esac
23+
24+
[[ -n "$file_path" ]] || exit 0
25+
case "$file_path" in *.groovy|*.java) ;; *) exit 0 ;; esac
26+
case "$file_path" in *modules/*|*plugins/*) ;; *) exit 0 ;; esac
27+
28+
# Resolve the gradle module path.
29+
module=""
30+
case "$file_path" in
31+
*plugins/nf-*)
32+
plugin=$(sed -E 's#.*plugins/(nf-[^/]+)/.*#\1#' <<<"$file_path")
33+
module="plugins:${plugin}" ;;
34+
*modules/nextflow/*) module="nextflow" ;;
35+
*modules/nf-commons/*) module="nf-commons" ;;
36+
*modules/nf-httpfs/*) module="nf-httpfs" ;;
37+
*modules/nf-lang/*) module="nf-lang" ;;
38+
*modules/nf-lineage/*) module="nf-lineage" ;;
39+
esac
40+
[[ -n "$module" ]] || exit 0
41+
42+
class=$(basename "$file_path"); class="${class%.*}"
43+
case "$class" in
44+
*Test) test_class="$class" ;;
45+
*) test_class="${class}Test" ;;
46+
esac
47+
48+
if out=$(./gradlew ":${module}:test" --tests "*${test_class}" 2>&1); then
49+
jq -n --arg f "$(basename "$file_path")" \
50+
'{suppressOutput: true, systemMessage: ("✓ Tests passed for " + $f)}'
51+
else
52+
summary=$(printf '%s\n' "$out" | grep -iE 'failed|error|exception|assertion' | tail -n 5)
53+
[[ -n "$summary" ]] || summary=$(printf '%s\n' "$out" | tail -n 15)
54+
jq -n --arg f "$(basename "$file_path")" --arg s "$summary" \
55+
'{decision: "block", reason: ("Tests failed for " + $f + ":\n" + $s)}'
56+
fi

.claude/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-editorconfig.sh",
1010
"timeout": 30,
1111
"async": true
12+
},
13+
{
14+
"type": "command",
15+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests.sh",
16+
"timeout": 300
1217
}
1318
]
1419
}

0 commit comments

Comments
 (0)