From 7d89e71464d5ba61b931a3a3acc64f789506fad5 Mon Sep 17 00:00:00 2001 From: yoma Date: Fri, 3 Jul 2026 12:09:39 +0800 Subject: [PATCH] Use portable timeout helper in test scripts --- tests/claude-code/run-skill-tests.sh | 5 +- tests/claude-code/test-helpers.sh | 5 +- ...subagent-driven-development-integration.sh | 3 +- tests/explicit-skill-requests/run-test.sh | 3 +- tests/opencode/test-priority.sh | 3 +- tests/opencode/test-tools.sh | 3 +- tests/support/with-timeout.sh | 50 +++++++++++++++++++ 7 files changed, 65 insertions(+), 7 deletions(-) create mode 100755 tests/support/with-timeout.sh diff --git a/tests/claude-code/run-skill-tests.sh b/tests/claude-code/run-skill-tests.sh index c94cfec9e0..7a2a226666 100755 --- a/tests/claude-code/run-skill-tests.sh +++ b/tests/claude-code/run-skill-tests.sh @@ -4,6 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WITH_TIMEOUT="$SCRIPT_DIR/../support/with-timeout.sh" cd "$SCRIPT_DIR" echo "========================================" @@ -120,7 +121,7 @@ for test in "${tests[@]}"; do start_time=$(date +%s) if [ "$VERBOSE" = true ]; then - if timeout "$TIMEOUT" bash "$test_path"; then + if "$WITH_TIMEOUT" "$TIMEOUT" bash "$test_path"; then end_time=$(date +%s) duration=$((end_time - start_time)) echo "" @@ -140,7 +141,7 @@ for test in "${tests[@]}"; do fi else # Capture output for non-verbose mode - if output=$(timeout "$TIMEOUT" bash "$test_path" 2>&1); then + if output=$("$WITH_TIMEOUT" "$TIMEOUT" bash "$test_path" 2>&1); then end_time=$(date +%s) duration=$((end_time - start_time)) echo " [PASS] (${duration}s)" diff --git a/tests/claude-code/test-helpers.sh b/tests/claude-code/test-helpers.sh index 1b5ead3b44..de63076c49 100755 --- a/tests/claude-code/test-helpers.sh +++ b/tests/claude-code/test-helpers.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # Helper functions for Claude Code skill tests +HELPER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WITH_TIMEOUT="$HELPER_DIR/../support/with-timeout.sh" + # Run Claude Code with a prompt and capture output # Usage: run_claude "prompt text" [timeout_seconds] [allowed_tools] run_claude() { @@ -16,7 +19,7 @@ run_claude() { fi # Run Claude in headless mode with timeout - if timeout "$timeout" "${cmd[@]}" > "$output_file" 2>&1; then + if "$WITH_TIMEOUT" "$timeout" "${cmd[@]}" > "$output_file" 2>&1; then cat "$output_file" rm -f "$output_file" return 0 diff --git a/tests/claude-code/test-subagent-driven-development-integration.sh b/tests/claude-code/test-subagent-driven-development-integration.sh index a783648314..22c03340c2 100755 --- a/tests/claude-code/test-subagent-driven-development-integration.sh +++ b/tests/claude-code/test-subagent-driven-development-integration.sh @@ -15,6 +15,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WITH_TIMEOUT="$SCRIPT_DIR/../support/with-timeout.sh" source "$SCRIPT_DIR/test-helpers.sh" echo "========================================" @@ -164,7 +165,7 @@ PLUGIN_DIR=$(cd "$SCRIPT_DIR/../.." && pwd) # other concurrent claude sessions. echo "Running Claude (plugin-dir: $PLUGIN_DIR, cwd: $TEST_PROJECT)..." echo "================================================================================" -cd "$TEST_PROJECT" && timeout 1800 claude -p "$PROMPT" --plugin-dir "$PLUGIN_DIR" --allowed-tools=all --permission-mode bypassPermissions 2>&1 | tee "$OUTPUT_FILE" || { +cd "$TEST_PROJECT" && "$WITH_TIMEOUT" 1800 claude -p "$PROMPT" --plugin-dir "$PLUGIN_DIR" --allowed-tools=all --permission-mode bypassPermissions 2>&1 | tee "$OUTPUT_FILE" || { echo "" echo "================================================================================" echo "EXECUTION FAILED (exit code: $?)" diff --git a/tests/explicit-skill-requests/run-test.sh b/tests/explicit-skill-requests/run-test.sh index 821117c39c..baaac56a36 100755 --- a/tests/explicit-skill-requests/run-test.sh +++ b/tests/explicit-skill-requests/run-test.sh @@ -23,6 +23,7 @@ fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Get the superpowers plugin root (two levels up) PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +WITH_TIMEOUT="$SCRIPT_DIR/../support/with-timeout.sh" TIMESTAMP=$(date +%s) OUTPUT_DIR="/tmp/superpowers-tests/${TIMESTAMP}/explicit-skill-requests/${SKILL_NAME}" @@ -68,7 +69,7 @@ echo "Running claude -p with explicit skill request..." echo "Prompt: $PROMPT" echo "" -timeout 300 claude -p "$PROMPT" \ +"$WITH_TIMEOUT" 300 claude -p "$PROMPT" \ --plugin-dir "$PLUGIN_DIR" \ --dangerously-skip-permissions \ --max-turns "$MAX_TURNS" \ diff --git a/tests/opencode/test-priority.sh b/tests/opencode/test-priority.sh index b9f3e08a8e..188d516a0e 100755 --- a/tests/opencode/test-priority.sh +++ b/tests/opencode/test-priority.sh @@ -7,6 +7,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WITH_TIMEOUT="$SCRIPT_DIR/../support/with-timeout.sh" OPENCODE_TEST_TIMEOUT_SECONDS="${OPENCODE_TEST_TIMEOUT_SECONDS:-120}" echo "=== Test: Skill Priority Resolution ===" @@ -107,7 +108,7 @@ run_opencode() { local exit_code set +e - command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1) + command_output=$(cd "$dir" && "$WITH_TIMEOUT" "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1) exit_code=$? set -e diff --git a/tests/opencode/test-tools.sh b/tests/opencode/test-tools.sh index da79f3d5ed..ecb37418af 100755 --- a/tests/opencode/test-tools.sh +++ b/tests/opencode/test-tools.sh @@ -6,6 +6,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WITH_TIMEOUT="$SCRIPT_DIR/../support/with-timeout.sh" OPENCODE_TEST_TIMEOUT_SECONDS="${OPENCODE_TEST_TIMEOUT_SECONDS:-120}" echo "=== Test: Tools Functionality ===" @@ -31,7 +32,7 @@ run_opencode() { local exit_code set +e - command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1) + command_output=$(cd "$dir" && "$WITH_TIMEOUT" "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1) exit_code=$? set -e diff --git a/tests/support/with-timeout.sh b/tests/support/with-timeout.sh new file mode 100755 index 0000000000..bfc11a6724 --- /dev/null +++ b/tests/support/with-timeout.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 SECONDS COMMAND [ARG...]" >&2 + exit 2 +fi + +duration="$1" +shift + +if command -v timeout >/dev/null 2>&1; then + exec timeout "$duration" "$@" +fi + +if command -v gtimeout >/dev/null 2>&1; then + exec gtimeout "$duration" "$@" +fi + +command -v python3 >/dev/null 2>&1 || { + echo "ERROR: timeout/gtimeout not found and python3 is unavailable" >&2 + exit 127 +} + +exec python3 - "$duration" "$@" <<'PY' +import subprocess +import sys + +raw_duration = sys.argv[1] +command = sys.argv[2:] + +try: + timeout_seconds = float(raw_duration[:-1] if raw_duration.endswith("s") else raw_duration) +except ValueError: + print(f"ERROR: invalid timeout duration: {raw_duration}", file=sys.stderr) + sys.exit(2) + +try: + completed = subprocess.run(command, timeout=timeout_seconds) +except subprocess.TimeoutExpired: + print(f"ERROR: command timed out after {raw_duration}", file=sys.stderr) + sys.exit(124) +except FileNotFoundError: + print(f"ERROR: command not found: {command[0]}", file=sys.stderr) + sys.exit(127) + +if completed.returncode < 0: + sys.exit(128 + abs(completed.returncode)) +sys.exit(completed.returncode) +PY