diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d69176..7c62e64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,12 @@ before the implementation. The loop is **type → red → green → refactor**: `npm run verify` as the definition-of-done gate. Every test pairs an `expectTypeOf(...)` with an `assert.*`; keep them together. +Each test body follows **AAA (Arrange–Act–Assert)** with labeled blocks +separated by a blank line: `// Arrange` sets up the inputs (e.g. the matcher +factory), `// Act` exercises the subject once from them (not a second +throwaway call), `// Assert` holds every check — type expectations first, +runtime assertions last; an empty block drops its label (see +[development/testing.md § AAA ordering](./development/testing.md#aaa-ordering)). Type-first is enforced structurally: `npm test` runs `check:tsc` before the test runner, so a wrong type can never be papered over by a passing assertion. Per [AGENTS.md § Never do](./AGENTS.md#never-do), reach green honestly — fix the diff --git a/development/testing.md b/development/testing.md index c58bbda..06a1986 100644 --- a/development/testing.md +++ b/development/testing.md @@ -36,6 +36,33 @@ The runner is `node --test --strip-types "src/**/*.test.ts"` and the tiers are i build step, and the runner relies on the `.ts` import-extension convention (see [tooling.md](./tooling.md#source-imports-use-ts-extensions)). +## AAA ordering + +The rule is in +[CONTRIBUTING.md § Testing discipline (type-driven)](../CONTRIBUTING.md#testing-discipline-type-driven). + +#### Decision (2026-09) + +Test bodies read arrange → act → assert: inputs (the factory) set up first, the +subject exercised once from them, all checks last — types then runtime. The +blocks are labeled with `// Arrange` / `// Act` / `// Assert` comments and +separated by a blank line; an empty block drops its label. + +#### Why + +- Interleaved setup/checks hide what runs vs. what is observed; the eye + re-reads the block to find the seams. +- A factory built mid-test invites a second throwaway call of the subject; + arranging it once makes the positive construction and the negative + `Parameters<…>` check share one source of truth. +- Labels make the seams explicit, not inferred — grep-able and reviewable + without reading the statements. + +#### Rejected + +- Unlabeled ordering (bare blank lines): the seams still have to be found by + reading; the labels cost nothing. + ## Known issues - The type-aware linter misidentifies `expectTypeOf()` as a floating promise, so