#!/bin/bash # test-hooks.sh — Unit tests for ms-ai-architect hook scripts # Usage: bash tests/test-hooks.sh set -euo pipefail PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" SCRIPTS_DIR="$PLUGIN_ROOT/hooks/scripts" PASS=0 FAIL=0 pass() { echo -e "\033[0;32m ✓ $1\033[0m"; PASS=$((PASS + 1)); } fail() { echo -e "\033[0;31m ✗ $1\033[0m"; FAIL=$((FAIL + 1)); } echo "=== Hook Script Tests ===" echo "" # ------------------------------------------------------- # pre-edit-secrets.mjs # ------------------------------------------------------- echo "--- pre-edit-secrets.mjs ---" # Test 1: clean content passes echo '{"tool_input":{"content":"Hello world","file_path":"readme.md"}}' \ | node "$SCRIPTS_DIR/pre-edit-secrets.mjs" 2>/dev/null \ && pass "Clean content: exit 0" \ || fail "Clean content: expected exit 0" # Test 2: Azure Storage key blocked (constructed at runtime to avoid hook self-trigger) AZURE_KEY_PAYLOAD=$(printf '{"tool_input":{"content":"%s","file_path":"config.ts"}}' \ "DefaultEndpointsProtocol=https;AccountName=test;AccountKey=$(printf 'a%.0s' {1..44})=") echo "$AZURE_KEY_PAYLOAD" \ | node "$SCRIPTS_DIR/pre-edit-secrets.mjs" 2>/dev/null \ && fail "Azure Storage key: expected exit 2 (blocked)" \ || pass "Azure Storage key: blocked correctly" # Test 3: GitHub token blocked (constructed at runtime) GH_TOKEN="ghp_$(printf 'a%.0s' {1..40})" printf '{"tool_input":{"content":"token: %s","file_path":"ci.yml"}}' "$GH_TOKEN" \ | node "$SCRIPTS_DIR/pre-edit-secrets.mjs" 2>/dev/null \ && fail "GitHub token: expected exit 2 (blocked)" \ || pass "GitHub token: blocked correctly" # Test 4: empty content passes echo '{"tool_input":{"content":"","file_path":"empty.md"}}' \ | node "$SCRIPTS_DIR/pre-edit-secrets.mjs" 2>/dev/null \ && pass "Empty content: exit 0" \ || fail "Empty content: expected exit 0" # Test 5: test files skipped (constructed at runtime) printf '{"tool_input":{"content":"api_key = \\"%s\\"","file_path":"auth.test.ts"}}' \ "$(printf 'x%.0s' {1..25})" \ | node "$SCRIPTS_DIR/pre-edit-secrets.mjs" 2>/dev/null \ && pass "Test file: skipped (exit 0)" \ || fail "Test file: should be skipped" echo "" # ------------------------------------------------------- # session-start-context.mjs # ------------------------------------------------------- echo "--- session-start-context.mjs ---" OUTPUT=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" node "$SCRIPTS_DIR/session-start-context.mjs" 2>/dev/null) EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then pass "Runs without error (exit 0)" else fail "Expected exit 0, got $EXIT_CODE" fi if echo "$OUTPUT" | grep -q "Architect:"; then pass "Output contains 'Architect:' prefix" else fail "Output missing 'Architect:' prefix" fi echo "" # ------------------------------------------------------- # stop-assessment-reminder.mjs # ------------------------------------------------------- echo "--- stop-assessment-reminder.mjs ---" # Test from a temp dir without .work/ TMPDIR_TEST=$(mktemp -d) OUTPUT=$(cd "$TMPDIR_TEST" && node "$SCRIPTS_DIR/stop-assessment-reminder.mjs" 2>/dev/null) EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then pass "No .work/: exit 0" else fail "No .work/: expected exit 0, got $EXIT_CODE" fi if [ "$OUTPUT" = "{}" ]; then pass "No .work/: returns {}" else fail "No .work/: expected {}, got: $OUTPUT" fi # Test with a fresh .work/ session mkdir -p "$TMPDIR_TEST/.work/test-session" touch "$TMPDIR_TEST/.work/test-session/state.json" OUTPUT=$(cd "$TMPDIR_TEST" && node "$SCRIPTS_DIR/stop-assessment-reminder.mjs" 2>/dev/null) if echo "$OUTPUT" | grep -q "systemMessage"; then pass "Fresh .work/: returns systemMessage" else fail "Fresh .work/: expected systemMessage in output" fi if echo "$OUTPUT" | grep -q "architect:adr"; then pass "Fresh .work/: suggests /architect:adr" else fail "Fresh .work/: missing /architect:adr suggestion" fi rm -rf "$TMPDIR_TEST" echo "" # ------------------------------------------------------- # Summary # ------------------------------------------------------- TOTAL=$((PASS + FAIL)) echo "=== Results: $PASS/$TOTAL passed, $FAIL failed ===" if [ "$FAIL" -gt 0 ]; then exit 1 fi