aboutsummaryrefslogtreecommitdiff
path: root/.claude
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-19 12:47:20 -0500
committerCraig Jennings <c@cjennings.net>2026-04-19 12:47:20 -0500
commit1ce2e7c14d41062edb85c84aef59645b5d7e9597 (patch)
tree8a46888e62883da6a789249210cd84c2f17efd61 /.claude
parent4beb93e4e774fc40d0c752973dc8744a06f0f2ff (diff)
downloaddotemacs-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.
Diffstat (limited to '.claude')
-rwxr-xr-x.claude/hooks/validate-el.sh16
-rw-r--r--.claude/rules/elisp-testing.md3
-rw-r--r--.claude/rules/testing.md153
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