From 9a61fbb3d9368a318fed21763423948b76991b81 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:18:14 -0500 Subject: [PATCH 1/3] refactor(content-guards): replace fragile bash expansion with explicit branching The previous fix (PR #39, commit 2202144) used ${config_flag[@]+"${config_flag[@]}"} to work around bash 3.2's unbound variable error with empty arrays. While functional, this expansion is: - Hard to read and understand - Easy to get wrong (even the commit message described it backwards) - Fragile across bash versions This change replaces the arcane parameter expansion with explicit conditional branching: - Check array length with ${#config_flag[@]} (works correctly under set -u) - Separate code paths for empty/non-empty arrays - Clear, maintainable, and works on all bash versions including macOS bash 3.2 Verified with /bin/bash (bash 3.2) under set -euo pipefail with both code paths. Co-Authored-By: Claude Sonnet 4.5 --- content-guards/scripts/validate-markdown.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/content-guards/scripts/validate-markdown.sh b/content-guards/scripts/validate-markdown.sh index 73dc0ab..09b58f3 100755 --- a/content-guards/scripts/validate-markdown.sh +++ b/content-guards/scripts/validate-markdown.sh @@ -104,7 +104,12 @@ EOF fi fi - if ! markdownlint_output=$(markdownlint-cli2 "${config_flag[@]+"${config_flag[@]}"}" "$file_path" 2>&1); then + if [[ ${#config_flag[@]} -gt 0 ]]; then + markdownlint_output=$(markdownlint-cli2 "${config_flag[@]}" "$file_path" 2>&1) + else + markdownlint_output=$(markdownlint-cli2 "$file_path" 2>&1) + fi + if [[ $? -ne 0 ]]; then errors+=("markdownlint-cli2 failed:") errors+=("$markdownlint_output") fi From f17a732ffae5aff2da0c60b486326525996b8c4a Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:30:30 -0500 Subject: [PATCH 2/3] test(content-guards): add bats-core test suite for markdown-validator Add comprehensive automated tests using bats-core to prevent regressions in the markdown validator hook, specifically targeting the unbound variable issue fixed in PR #39. Test coverage: - File type filtering (non-markdown, missing, dotfiles) - Config resolution (project vs fallback) - Cross-repo editing scenarios (CWD != file's directory) - Unbound variable regression prevention - Empty JSON input handling This is the first automated test suite in the Claude Code plugin ecosystem. Structure: - tests/content-guards/markdown-validator/validate-markdown.bats (8 tests) - tests/content-guards/markdown-validator/fixtures/ (test data) - content-guards/README.md updated with testing documentation All 8 tests pass successfully. Co-Authored-By: Claude Sonnet 4.5 --- content-guards/README.md | 18 +++++ .../cross-repo-sim/feat/new-feature/README.md | 5 ++ .../project-with-config/.markdownlint.json | 6 ++ .../fixtures/project-with-config/doc.md | 5 ++ .../fixtures/valid-basic.md | 6 ++ .../markdown-validator/validate-markdown.bats | 81 +++++++++++++++++++ 6 files changed, 121 insertions(+) create mode 100644 tests/content-guards/markdown-validator/fixtures/cross-repo-sim/feat/new-feature/README.md create mode 100644 tests/content-guards/markdown-validator/fixtures/project-with-config/.markdownlint.json create mode 100644 tests/content-guards/markdown-validator/fixtures/project-with-config/doc.md create mode 100644 tests/content-guards/markdown-validator/fixtures/valid-basic.md create mode 100644 tests/content-guards/markdown-validator/validate-markdown.bats 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/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" ] +} From 0469b222c14fe7d97236b9d406ee560d192d9803 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 16 Feb 2026 05:17:52 -0500 Subject: [PATCH 3/3] fix(content-guards): correct exit code capture in markdownlint validation Addresses review feedback from copilot-pull-request-reviewer and gemini-code-assist. The previous implementation incorrectly checked $? after the if-else block, which captured the exit code of the conditional test [[ ${#config_flag[@]} -gt 0 ]] instead of the markdownlint-cli2 command. This meant: - If array check was true (exit 0), errors wouldn't be captured even if markdownlint failed - If array check was false (exit 1), errors would be incorrectly captured even if markdownlint succeeded Fixed by using the `if ! command_output=$(command ...)` pattern consistently, matching the cspell implementation on line 120. While this introduces some duplication, it's the most reliable approach for bash 3.2 with set -u. Verified with bats test suite - all 8 tests pass. Co-Authored-By: Claude Sonnet 4.5 --- content-guards/scripts/validate-markdown.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/content-guards/scripts/validate-markdown.sh b/content-guards/scripts/validate-markdown.sh index 09b58f3..46e2067 100755 --- a/content-guards/scripts/validate-markdown.sh +++ b/content-guards/scripts/validate-markdown.sh @@ -105,13 +105,15 @@ EOF fi if [[ ${#config_flag[@]} -gt 0 ]]; then - markdownlint_output=$(markdownlint-cli2 "${config_flag[@]}" "$file_path" 2>&1) + if ! markdownlint_output=$(markdownlint-cli2 "${config_flag[@]}" "$file_path" 2>&1); then + errors+=("markdownlint-cli2 failed:") + errors+=("$markdownlint_output") + fi else - markdownlint_output=$(markdownlint-cli2 "$file_path" 2>&1) - fi - if [[ $? -ne 0 ]]; then - errors+=("markdownlint-cli2 failed:") - errors+=("$markdownlint_output") + if ! markdownlint_output=$(markdownlint-cli2 "$file_path" 2>&1); then + errors+=("markdownlint-cli2 failed:") + errors+=("$markdownlint_output") + fi fi fi