diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-19 12:47:20 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-19 12:47:20 -0500 |
| commit | 1ce2e7c14d41062edb85c84aef59645b5d7e9597 (patch) | |
| tree | 8a46888e62883da6a789249210cd84c2f17efd61 | |
| parent | 4beb93e4e774fc40d0c752973dc8744a06f0f2ff (diff) | |
| download | dotemacs-1ce2e7c14d41062edb85c84aef59645b5d7e9597.tar.gz dotemacs-1ce2e7c14d41062edb85c84aef59645b5d7e9597.zip | |
chore: sync .claude/ bundle — package-initialize, flat-layout, generic testing
Re-installed the elisp ruleset from ~/code/rulesets, picking up three
upstream bundle fixes:
- validate-el.sh now calls (package-initialize) so byte-compile can
resolve external packages (dash, etc.) via ~/.emacs.d/elpa/.
- validate-el.sh Phase 2 (test runner) now matches any .el file
outside tests/, not just modules/*.el. Supports flat-layout
projects (Elisp package repos where sources live at project root).
- .claude/rules/testing.md is now generic TDD principles (was
Python/TS specific); language-specific testing rules live in
elisp-testing.md, python-testing.md, etc.
elisp-testing.md gained a line referencing testing.md as the base.
| -rwxr-xr-x | .claude/hooks/validate-el.sh | 16 | ||||
| -rw-r--r-- | .claude/rules/elisp-testing.md | 3 | ||||
| -rw-r--r-- | .claude/rules/testing.md | 153 |
3 files changed, 168 insertions, 4 deletions
diff --git a/.claude/hooks/validate-el.sh b/.claude/hooks/validate-el.sh index 5fd42416..6f93d485 100755 --- a/.claude/hooks/validate-el.sh +++ b/.claude/hooks/validate-el.sh @@ -34,6 +34,7 @@ case "$f" in -L "$PROJECT_ROOT" \ -L "$PROJECT_ROOT/modules" \ -L "$PROJECT_ROOT/tests" \ + --eval '(package-initialize)' \ "$f" \ --eval '(check-parens)' \ --eval "(or (byte-compile-file \"$f\") (kill-emacs 1))" 2>&1)"; then @@ -44,12 +45,13 @@ case "$f" in esac # --- Phase 2: test runner --- -# Determine which tests (if any) apply to this edit. +# Determine which tests (if any) apply to this edit. Works for projects with +# source at root, in modules/, or elsewhere — stem-based test lookup is the +# common pattern. tests=() case "$f" in - "$PROJECT_ROOT/modules/"*.el) - stem="$(basename "${f%.el}")" - mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort) + */init.el|*/early-init.el) + : # Phase 1 handled it; skip test runner ;; "$PROJECT_ROOT/tests/testutil-"*.el) stem="$(basename "${f%.el}")" @@ -59,6 +61,11 @@ case "$f" in "$PROJECT_ROOT/tests/test-"*.el) tests=("$f") ;; + *.el) + # Any other .el under the project — find matching tests by stem + stem="$(basename "${f%.el}")" + mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort) + ;; esac count="${#tests[@]}" @@ -69,6 +76,7 @@ if [ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]; then -L "$PROJECT_ROOT" \ -L "$PROJECT_ROOT/modules" \ -L "$PROJECT_ROOT/tests" \ + --eval '(package-initialize)' \ -l ert "${load_args[@]}" \ --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then printf 'TESTS FAILED for %s (%d test file(s)):\n%s\n' "$f" "$count" "$output" >&2 diff --git a/.claude/rules/elisp-testing.md b/.claude/rules/elisp-testing.md index fcad9de1..6cb59b1c 100644 --- a/.claude/rules/elisp-testing.md +++ b/.claude/rules/elisp-testing.md @@ -2,6 +2,9 @@ Applies to: `**/tests/*.el` +Implements the core principles from `testing.md`. All rules there apply here — +this file covers Elisp-specific patterns. + ## Framework: ERT Use `ert-deftest` for all tests. One test = one scenario. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 00000000..42cc5281 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,153 @@ +# Testing Standards + +Applies to: `**/*` + +Core TDD discipline and test quality rules. Language-specific patterns +(frameworks, fixture idioms, mocking tools) live in per-language testing files +under `languages/<lang>/claude/rules/`. + +## Test-Driven Development (Default) + +TDD is the default workflow for all code, including demos and prototypes. **Write tests first, before any implementation code.** Tests are how you prove you understand the problem — if you can't write a failing test, you don't yet understand what needs to change. + +1. **Red**: Write a failing test that defines the desired behavior +2. **Green**: Write the minimal code to make the test pass +3. **Refactor**: Clean up while keeping tests green + +Do not skip TDD for demo code. Demos build muscle memory — the habit carries into production. + +### Understand Before You Test + +Before writing tests, invest time in understanding the code: + +1. **Explore the codebase** — Read the module under test, its callers, and its dependencies. Understand the data flow end to end. +2. **Identify the root cause** — If fixing a bug, trace the problem to its origin. Don't test (or fix) surface symptoms when the real issue is deeper in the call chain. +3. **Reason through edge cases** — Consider boundary conditions, error states, concurrent access, and interactions with adjacent modules. Your tests should cover what could actually go wrong, not just the obvious happy path. + +### Adding Tests to Existing Untested Code + +When working in a codebase without tests: + +1. Write a **characterization test** that captures current behavior before making changes +2. Use the characterization test as a safety net while refactoring +3. Then follow normal TDD for the new change + +## Test Categories (Required for All Code) + +Every unit under test requires coverage across three categories: + +### 1. Normal Cases (Happy Path) +- Standard inputs and expected use cases +- Common workflows and default configurations +- Typical data volumes + +### 2. Boundary Cases +- Minimum/maximum values (0, 1, -1, MAX_INT) +- Empty vs null vs undefined (language-appropriate) +- Single-element collections +- Unicode and internationalization (emoji, RTL text, combining characters) +- Very long strings, deeply nested structures +- Timezone boundaries (midnight, DST transitions) +- Date edge cases (leap years, month boundaries) + +### 3. Error Cases +- Invalid inputs and type mismatches +- Network failures and timeouts +- Missing required parameters +- Permission denied scenarios +- Resource exhaustion +- Malformed data + +## Test Organization + +Typical layout: + +``` +tests/ + unit/ # One test file per source file + integration/ # Multi-component workflows + e2e/ # Full system tests +``` + +Per-language files may adjust this (e.g. Elisp collates ERT tests into +`tests/test-<module>*.el` without subdirectories). + +## Naming Convention + +- Unit: `test_<module>_<function>_<scenario>_<expected>` +- Integration: `test_integration_<workflow>_<scenario>_<outcome>` + +Examples: +- `test_cart_apply_discount_expired_coupon_raises_error` +- `test_integration_order_sync_network_timeout_retries_three_times` + +Languages that prefer camelCase, kebab-case, or other conventions keep the +structure but use their idiom. Consistency within a project matters more than +the specific case choice. + +## Test Quality + +### Independence +- No shared mutable state between tests +- Each test runs successfully in isolation +- Explicit setup and teardown + +### Determinism +- Never hardcode dates or times — generate them relative to `now()` +- No reliance on test execution order +- No flaky network calls in unit tests + +### Performance +- Unit tests: <100ms each +- Integration tests: <1s each +- E2E tests: <10s each +- Mark slow tests with appropriate decorators/tags + +### Mocking Boundaries +Mock external dependencies at the system boundary: +- Network calls (HTTP, gRPC, WebSocket) +- File I/O and cloud storage +- Time and dates +- Third-party service clients + +Never mock: +- The code under test +- Internal domain logic +- Framework behavior (ORM queries, middleware, hooks, buffer primitives) + +## Coverage Targets + +- Business logic and domain services: **90%+** +- API endpoints and views: **80%+** +- UI components: **70%+** +- Utilities and helpers: **90%+** +- Overall project minimum: **80%+** + +New code must not decrease coverage. PRs that lower coverage require justification. + +## TDD Discipline + +TDD is non-negotiable. These are the rationalizations agents use to skip it — don't fall for them: + +| Excuse | Why It's Wrong | +|--------|----------------| +| "This is too simple to need a test" | Simple code breaks too. The test takes 30 seconds. Write it. | +| "I'll add tests after the implementation" | You won't, and even if you do, they'll test what you wrote rather than what was needed. Test-after validates implementation, not behavior. | +| "Let me just get it working first" | That's not TDD. If you can't write a failing test, you don't understand the requirement yet. | +| "This is just a refactor" | Refactors without tests are guesses. Write a characterization test first, then refactor while it stays green. | +| "I'm only changing one line" | One-line changes cause production outages. Write a test that covers the line you're changing. | +| "The existing code has no tests" | Start with a characterization test. Don't make the problem worse. | +| "This is demo/prototype code" | Demos build habits. Untested demo code becomes untested production code. | +| "I need to spike first" | Spikes are fine — then throw away the spike, write the test, and implement properly. | + +If you catch yourself thinking any of these, stop and write the test. + +## Anti-Patterns (Do Not Do) + +- Hardcoded dates or timestamps (they rot) +- Testing implementation details instead of behavior +- Mocking the thing you're testing +- Shared mutable state between tests +- Non-deterministic tests (random without seed, network in unit tests) +- Testing framework behavior instead of your code +- Ignoring or skipping failing tests without a tracking issue |
