diff --git a/content-guards/README.md b/content-guards/README.md index 6f2e944..cedb33c 100644 --- a/content-guards/README.md +++ b/content-guards/README.md @@ -23,6 +23,24 @@ claude plugins add jacobpevans-cc-plugins/content-guards - `cspell` - Spell checking - `gh` - GitHub CLI +## Testing + +The markdown-validator has automated tests using [bats-core](https://github.com/bats-core/bats-core): + +```bash +# Install bats-core (if not already installed) +brew install bats-core + +# Run the test suite from repo root +bats tests/content-guards/markdown-validator/validate-markdown.bats +``` + +Test coverage includes: +- File type filtering (non-markdown, missing, dotfiles) +- Config resolution (project vs fallback) +- Cross-repo editing scenarios +- Unbound variable regression prevention (PR #39, #40) + ## License Apache-2.0 diff --git a/content-guards/scripts/validate-markdown.sh b/content-guards/scripts/validate-markdown.sh index 73dc0ab..46e2067 100755 --- a/content-guards/scripts/validate-markdown.sh +++ b/content-guards/scripts/validate-markdown.sh @@ -104,9 +104,16 @@ EOF fi fi - if ! markdownlint_output=$(markdownlint-cli2 "${config_flag[@]+"${config_flag[@]}"}" "$file_path" 2>&1); then - errors+=("markdownlint-cli2 failed:") - errors+=("$markdownlint_output") + if [[ ${#config_flag[@]} -gt 0 ]]; then + if ! markdownlint_output=$(markdownlint-cli2 "${config_flag[@]}" "$file_path" 2>&1); then + errors+=("markdownlint-cli2 failed:") + errors+=("$markdownlint_output") + fi + else + if ! markdownlint_output=$(markdownlint-cli2 "$file_path" 2>&1); then + errors+=("markdownlint-cli2 failed:") + errors+=("$markdownlint_output") + fi fi fi diff --git a/tests/content-guards/markdown-validator/fixtures/cross-repo-sim/feat/new-feature/README.md b/tests/content-guards/markdown-validator/fixtures/cross-repo-sim/feat/new-feature/README.md new file mode 100644 index 0000000..fbba89d --- /dev/null +++ b/tests/content-guards/markdown-validator/fixtures/cross-repo-sim/feat/new-feature/README.md @@ -0,0 +1,5 @@ +# Cross-Repo Feature + +This file simulates a cross-repo editing scenario. + +It has no ancestor `.markdownlint*` config, so the validator should use the fallback config. diff --git a/tests/content-guards/markdown-validator/fixtures/project-with-config/.markdownlint.json b/tests/content-guards/markdown-validator/fixtures/project-with-config/.markdownlint.json new file mode 100644 index 0000000..439efe7 --- /dev/null +++ b/tests/content-guards/markdown-validator/fixtures/project-with-config/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD013": { + "line_length": 120 + } +} diff --git a/tests/content-guards/markdown-validator/fixtures/project-with-config/doc.md b/tests/content-guards/markdown-validator/fixtures/project-with-config/doc.md new file mode 100644 index 0000000..2a3b60f --- /dev/null +++ b/tests/content-guards/markdown-validator/fixtures/project-with-config/doc.md @@ -0,0 +1,5 @@ +# Project Documentation + +This markdown file lives in a directory with a `.markdownlint.json` config. + +It should be validated using that project config. diff --git a/tests/content-guards/markdown-validator/fixtures/valid-basic.md b/tests/content-guards/markdown-validator/fixtures/valid-basic.md new file mode 100644 index 0000000..d18e3a6 --- /dev/null +++ b/tests/content-guards/markdown-validator/fixtures/valid-basic.md @@ -0,0 +1,6 @@ +# Valid Basic Markdown + +This is a minimal valid markdown file for testing. + +- Item 1 +- Item 2 diff --git a/tests/content-guards/markdown-validator/validate-markdown.bats b/tests/content-guards/markdown-validator/validate-markdown.bats new file mode 100644 index 0000000..27e5b10 --- /dev/null +++ b/tests/content-guards/markdown-validator/validate-markdown.bats @@ -0,0 +1,81 @@ +#!/usr/bin/env bats +# Test suite for content-guards/scripts/validate-markdown.sh +# +# Tests the markdown validator hook behavior including: +# - File type filtering (non-markdown, missing, dotfiles) +# - Config resolution (project vs fallback) +# - Cross-repo editing scenarios +# - Unbound variable regression (PR #39, #40) +# +# Run with: bats tests/content-guards/markdown-validator/validate-markdown.bats + +setup() { + # Path to the script under test (relative to repo root) + REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../.." && pwd)" + SCRIPT="$REPO_ROOT/content-guards/scripts/validate-markdown.sh" + FIXTURES="$(dirname "$BATS_TEST_FILENAME")/fixtures" + + # Verify script exists + if [[ ! -f "$SCRIPT" ]]; then + echo "ERROR: Script not found at $SCRIPT" >&2 + return 1 + fi +} + +@test "TC1: non-markdown file is skipped" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"test.py\"}}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "TC2: missing file is skipped" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"/nonexistent/file.md\"}}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "TC3: home dotfile is skipped" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"~/.config/foo.md\"}}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "TC4: .claude directory file is skipped" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"/path/to/.claude/foo.md\"}}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "TC5: empty config_flag with project config does not cause unbound variable" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"'"$FIXTURES"'/project-with-config/doc.md\"}}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [[ ! "$output" =~ "unbound variable" ]] +} + +@test "TC6: non-empty config_flag with fallback config" { + # File without ancestor config should use fallback (temp config or plugin default) + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"'"$FIXTURES"'/cross-repo-sim/feat/new-feature/README.md\"}}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [[ ! "$output" =~ "unbound variable" ]] +} + +@test "TC7: cross-repo editing finds config from file directory not CWD" { + # Simulate being in a completely different directory (like ~/git/repo1) + # while editing a file in ~/git/repo2/feat/branch/ + original_dir=$(pwd) + cd /tmp + + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"'"$FIXTURES"'/project-with-config/doc.md\"}}" | /bin/bash "$1"' _ "$SCRIPT" + + # Restore original directory + cd "$original_dir" + + [ "$status" -eq 0 ] + [[ ! "$output" =~ "unbound variable" ]] +} + +@test "TC8: empty JSON input is handled gracefully" { + run bash -c 'echo "{}" | /bin/bash "$1"' _ "$SCRIPT" + [ "$status" -eq 0 ] + [ -z "$output" ] +}