استخدم PostToolUse Hooks على أدوات Write/Edit لتشغيل الاختبارات بعد كل تغيير — النتائج الفاشلة تُعاد إلى Claude ليصلحها في الحال.
Claude Code يعدّل الكود بسرعة — لكنه لا يُشغّل الاختبارات من تلقاء نفسه. افتراضياً، بمجرد كتابة ملف يعتبر المهمة منتهية. دون تعليمات صريحة، لن يعرف إذا كان التغيير قد كسر شيئاً.
خطّاف PostToolUse يحل هذه المشكلة: في كل مرة يكتب فيها Claude ملفاً، تعمل الاختبارات تلقائياً. عند الفشل، تُعاد النتيجة مباشرةً إلى Claude ليصلحها في الحال.
يعدّل Claude Code الملفات بأداتين: Write (إنشاء أو استبدال) وEdit (تغييرات جزئية). ربط خطّاف بمرحلة PostToolUse لهاتين الأداتين يُطلق تشغيل الاختبارات في كل مرة يُحفظ فيها ملف على القرص.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/run-tests.sh"
}
]
}
]
}
}
يستقبل الخطّاف JSON كامل لاستدعاء الأداة عبر stdin، بما فيه مسار الملف المعدَّل. استخدام هذا المسار لتحديد الاختبارات التي ستُشغَّل هو مفتاح هذا النهج.
قواعد رمز الخروج (PostToolUse):
- exit 0: الاختبارات نجحت — يكمل Claude دون انقطاع
- exit 2: الاختبارات فشلت — محتوى stderr يُحقن إلى Claude الذي يرى الخطأ ويحاول التصحيح
أبسط نهج: فحص امتداد الملف وتشغيل مجموعة الاختبارات المناسبة.
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[[ -z "$file" ]] && exit 0
# لا تُطلق الاختبارات عند تعديل ملف الاختبار نفسه
echo "$file" | grep -qE '(test|spec)\.' && exit 0
case "$file" in
*.py)
cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
result=$(python -m pytest --tb=short -q 2>&1)
;;
*.ts|*.js|*.tsx|*.jsx)
cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
result=$(npm test -- --passWithNoTests 2>&1)
;;
*.go)
dir=$(dirname "$file")
result=$(go test "./$dir/..." 2>&1)
;;
*.rb)
cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
result=$(bundle exec rspec --format progress 2>&1)
;;
*)
exit 0
;;
esac
exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "فشلت الاختبارات (الملف المعدَّل: $file):" >&2
echo "$result" >&2
exit 2
fi
تشغيل المجموعة كاملة في كل تغيير بطيء جداً. استخدم مسار الملف المعدَّل لإيجاد الاختبارات ذات الصلة فقط.
في مشروع Python، يقابل src/auth/login.py عادةً tests/auth/test_login.py:
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[[ -z "$file" || "$file" != *.py ]] && exit 0
echo "$file" | grep -qE '(test_|_test\.py)' && exit 0
root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"
basename=$(basename "$file" .py)
test_file=$(find tests -name "test_${basename}.py" 2>/dev/null | head -1)
if [[ -n "$test_file" ]]; then
result=$(python -m pytest "$test_file" --tb=short -q 2>&1)
else
result=$(python -m pytest --tb=short -q 2>&1)
fi
if [[ $? -ne 0 ]]; then
echo "فشلت الاختبارات:" >&2
echo "$result" >&2
exit 2
fi
لمشاريع JavaScript/TypeScript، يدعم Jest التصفية بنمط اسم الملف:
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[[ -z "$file" ]] && exit 0
echo "$file" | grep -qE '\.(test|spec)\.' && exit 0
echo "$file" | grep -qE '\.(ts|tsx|js|jsx)$' || exit 0
root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"
basename=$(basename "$file" | sed 's/\.[^.]*$//')
result=$(npx jest --testPathPattern="$basename" --passWithNoTests 2>&1)
if [[ $? -ne 0 ]]; then
echo "فشلت الاختبارات:" >&2
echo "$result" >&2
exit 2
fi
تغطية Write وEdit مع حماية بمهلة زمنية لمنع الاختبارات المعلّقة من تجميد Claude:
.claude/hooks/run-tests.sh:
#!/bin/bash
set -euo pipefail
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[[ -z "$file" ]] && exit 0
# تخطي ملفات الاختبار نفسها والملفات غير البرمجية
echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0
echo "$file" | grep -qE '\.(py|ts|js|tsx|jsx|go|rb)$' || exit 0
root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"
run_test() {
timeout 60 "$@" 2>&1
}
case "$file" in
*.py)
result=$(run_test python -m pytest --tb=short -q)
;;
*.ts|*.tsx|*.js|*.jsx)
basename=$(basename "$file" | sed 's/\.[^.]*$//')
result=$(run_test npx jest --testPathPattern="$basename" --passWithNoTests)
;;
*.go)
dir=$(dirname "$file")
result=$(run_test go test "./$dir/...")
;;
*.rb)
result=$(run_test bundle exec rspec --format progress)
;;
*)
exit 0
;;
esac
if [[ $? -ne 0 ]]; then
echo "⚠ فشلت الاختبارات (الملف المحرِّك: $(basename "$file"))" >&2
echo "" >&2
echo "$result" >&2
exit 2
fi
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/run-tests.sh"
}
]
}
]
}
}
لا تُطلق الاختبارات عند تعديل ملفات الاختبار
عندما يعدّل Claude ملف اختبار، الاختبار نفسه لم يكتمل بعد. تشغيله الآن سابق لأوانه. هذا السطر التصفوي ضروري:
echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0
Write وEdit يستخدمان أسماء حقول مختلفة في tool_input
Write يستخدم file_path، وEdit يستخدم path. التعامل معهما بقيمة احتياطية:
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
التحكم في مهلة الاختبارات
مجموعات الاختبار البطيئة مع تعديلات Claude المتكررة قد تُجمّد الجلسة. timeout 60 قيمة معقولة؛ اضبطها وفق مدة الاختبار الفعلية للمشروع.
مستوى المشروع، لا العالمي
هذا الخطّاف ينتمي إلى إعداد مستوى المشروع (.claude/settings.json). تتفاوت أوامر الاختبار كثيراً بين المشاريع. للإصدار العالمي، يحتاج البرنامج النصي إلى اكتشاف نوع المشروع ديناميكياً.
مع هذا الإعداد، يصبح إيقاع عمل Claude:
لا حاجة للتكرار المستمر "شغّل الاختبارات". لكل تغيير يُجريه Claude شبكة أمان من الاختبارات.