# Testing For this library the types _are_ the feature — narrowing, `exhaustive()` returns, the `Matcher` contract — so a runtime-only test loop would verify the wrong thing. The contributor-facing commands are in [CONTRIBUTING.md](../CONTRIBUTING.md); this file records why the loop is shaped the way it is. ## Type-driven development New behavior follows **type-driven development** (in Edwin Brady's sense): _treat the type as the plan for a program, and use the compiler and type checker as your assistant, guiding you to a complete program that satisfies the type_ ([idris-lang.org](https://www.idris-lang.org/)). Here that plan is the `expectTypeOf` assertion, written first. The loop is **type → red → green → refactor**: 1. **Type** — write the compile-time expectation first (`expectTypeOf(...).toEqualTypeOf<…>()`) and let `npm run check:tsc` fail on the _type_. The type error is the spec you want to hit before the runtime logic exists. 2. **Red** — add the matching runtime assertion (`assert.*`) so `npm run test:unit` now fails on behavior. 3. **Green** — implement in `src/*.ts` until both the type check and the test pass. 4. **Refactor** — with the type system and the tests as the safety net, then `npm run verify` as the definition-of-done gate. This is why every test in the suite pairs an `expectTypeOf(...)` with an `assert.*` — keep them together. #### Decision (2026-09) Test-driven development is _type_-driven here: the compile-time expectation is written before the runtime assertion, and both before the implementation. #### Why - A runtime-only test can pass while the type is wrong, and a type-level library would then ship a broken feature that its tests bless. - The type error is a more precise spec than a failing assertion, because it states the exact expected type before the logic exists. #### Rejected - Runtime-first (classic red/green): it verifies the value, not the contract, and the contract is the product. - Testing the type only: it would not catch handler wiring, `exhaustive()` throwing, or the `otherwise` fallback (see `src/index.test.ts`). ## Enforcement Type-first is also 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 types so both the type check and the runtime assertion pass, never suppress the ones you can't make pass. ## Test tiers - `npm run test:unit` — the test runner alone, for the fast local loop. - `npm test` — `check:tsc` + the unit suite; this is what pre-push and the baseline check run. - `npm run test:ci` — adds c8 coverage; used by CI. `c8` uses V8 coverage, so the `--strip-types` source is instrumented without a build step. The runner is `node --test --strip-types "src/**/*.test.ts"`. It relies on the `.ts` import-extension convention (see [tooling.md](./tooling.md#source-imports-use-ts-extensions)). #### Known issue - The type-aware linter misidentifies `expectTypeOf()` as a floating promise, so `src/index.test.ts` carries a file-level `oxlint-disable typescript/no-floating-promises` with an explanatory comment. This is a known false positive, not a rule we want off project-wide (see [tooling.md § oxlint-disable directives live next to the code](./tooling.md#oxlint-disable-directives-live-next-to-the-code)).