# Testing For this library the types _are_ the feature, so a runtime-only test loop would verify the wrong thing. The commands are in [CONTRIBUTING.md](../CONTRIBUTING.md); this file records why the loop is shaped the way it is. ## Type-driven development The rules — the loop and the pairing rule — are in [CONTRIBUTING.md § Testing discipline (type-driven)](../CONTRIBUTING.md#testing-discipline-type-driven). What follows is why and what was rejected. #### Decision (2026-09) 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, so a type-level library would ship a broken feature 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 dispatch or the `_` fallback (see `src/primitive-union.test.ts`). The runner is `node --test --strip-types "src/**/*.test.ts"` and the tiers are in [CONTRIBUTING.md § Development commands](../CONTRIBUTING.md#development-commands). `c8` uses V8 coverage, so the `--strip-types` source is instrumented without a build step, and the runner relies on the `.ts` import-extension convention (see [tooling.md](./tooling.md#source-imports-use-ts-extensions)). ## Handler arguments #### Decision (2026-09) A handler's `expectTypeOf(shape)` is always paired with an assertion on the argument `dispatch` actually passed: `assert.equal` where the handler runs for one shape, `assert.ok(s === … || s === …)` over the set a `_` fallback accepts (`assert` is imported as `strict`, so each comparison is `Object.is`). Where a test should also prove that the _exact_ value reached the handler unchanged, the fallback returns the shape verbatim and the call site asserts it. #### Why - The parameter's type is what the compiler inferred from the pattern; the argument is what the runtime passed. Only the second can drift, and the keys whose property name differs from their value (`true`, `null`, `1`) are exactly where it can — see [library.md](./library.md). - `String(shape)` at the call site keeps every type expectation and every return-value assertion green; the argument assertions fail (10 of the 33 tests). Without them the suite never looks at the passed argument. - Returning the shape verbatim costs a widening pattern nothing: the remainder type joins the union of handler returns in place of a marker literal, so the test still shows the widening it is named for. #### Rejected - A recorded `unknown[]` of every fallback call compared with `deepEqual`: strong, but it couples the assertion to call order, and the sink sits three blocks away from the value it observes. - `typeof` checks: they cannot separate `2` from its key text `"2"`, which is the drift a fallback with a numeric remainder can hit. - One expected value asserted inline in a fallback: its argument is a _set_ of shapes, so only the disjunction holds on every call. ## 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. ## Test helpers The rule is enforced by `import/no-relative-parent-imports`; this section records why the mechanism is shaped like this. #### Decision (2026-09) A shared test helper — code that scattered `*.test.ts` files import to do their testing — lives under `src/util/__tests__/` and is addressed by the `#test-utils/…` self-reference (`package.json#imports`: `"#test-utils/*": "./src/util/__tests__/*"`), never by a relative path: ```ts import { LspSession } from "#test-utils/lsp-completion.ts"; ``` #### Why - The lint rule bans upward (`../`) imports, and a cross-cutting helper can always be placed above _some_ scattered consumer, wherever it goes. Name beats path: a `#test-utils/…` specifier has no direction, so the rule never fires and file moves only touch the one mapping in `package.json`. - `#…` is Node's reserved prefix for _private_ subpath imports: publishing `package.json` leaks nothing and resolves nothing for consumers. - `__tests__` as the folder name is not about tests living there; it is the directory pattern `tsconfig.build.json` already excludes, so a helper can never be emitted into `dist/` and shipped by accident. - Node's own resolver handles `#…` under `--strip-types`, and `tsc` resolves it via the same `imports` field — one mechanism for runtime and type gate, no loader needed. - The helper uses `node:` builtins (it drives a language server), so `import/no-nodejs-modules` is off for `src/util/__tests__/**` in `.oxlintrc.json` — the same exception the `scripts/**` scope carries. - Helpers carry no library coupling: their tests probe in-memory documents whose contextual types are written inline, so only the helper's own contract (marker handling, position, label extraction) is under test. A test that asserts a _matcher's_ popup belongs with the matcher. #### Rejected - A helper beside the tests (`src/util/test.ts`, flat `src/`): composes only while `src/` stays flat; the first nested test reaching it reintroduces the banned upward import. - Bare `~/…` specifier: not valid in `imports` (keys must start with `#`) — resolution fails at runtime with `ERR_MODULE_NOT_FOUND`. A `#~/…` “home” shorthand was dropped in review; `#test-utils/…` states what it is. - tsconfig `paths` alias: resolves for `tsc` but not for plain `node --test --strip-types` (no loader hook), breaking the fast tier. - Turning `import/no-relative-parent-imports` off for test files: the rule still guards non-test helpers importing each other, and the exemption is only needed for the one specifier the mapping already solves cleanly. ## Autocomplete #### Decision (2026-09) Completion is verified by driving the repo's own language server (`tsc --lsp --stdio`, the same server pi's LSP extension talks to) through the test helper `#test-utils/lsp-completion.ts` (`src/util/__tests__/lsp-completion.ts`), not through the type system: ```sh node --strip-types src/util/__tests__/lsp-completion.ts [] ``` The script prints the labels the server offers at a `/*COMPLETE*/` marker inside `` (the marker is stripped before the document is sent). Its `LspSession` is imported by `src/util/__tests__/lsp-completion.test.ts` — which tests the helper itself against inline documents, never the library's code — and by `src/primitive-union.test.ts`, where the same probe asserts the matcher's popup; the CLI is for manual inspection. #### Why - Completion is a contextual-type property: it depends on which overload signature TypeScript picks for the object literal, and no type-level assertion observes that. - `Parameters[0]` resolves only the _last_ overload, so it is not the popup's contextual type either — see [library.md § Matcher shape](./library.md#matcher-shape). - The server is the only ground truth; the script reproduces what the editor shows. #### Rejected - **`expect-type` would not work**: there is no operator for “the popup offers these labels”. `toExtend` / `toEqualTypeOf` test assignability and cannot say which overload supplied the contextual type. - **Checking by hand in the editor**: not reproducible in review or by an agent. - **`@ts-expect-error` at a completion position**: it asserts the absence of a compile error, not the presence of specific labels. #### Known issue - Each test spawns its own `tsc` server so the tests share no state and pass in any order; the file is an integration test (~1.6 s) that needs `node_modules`. `didOpen` is handled in order before the completion request, so no settle delay is needed. - The server answers some requests with a string id (`client/registerCapability`); the client must tolerate `string | number` ids or the server stalls. - `LspSession.close()` sends `shutdown` and then closes stdin instead of sending `exit`. The TS 7 Go server's `handleExit` returns `io.EOF`, cancelling the background context while a watch update is still in flight, and logs a bare `context canceled` before exiting 1; EOF on stdin exits 0 with no output. The kill stays as a fallback for a server that does not exit. ## Known issues - The type-aware linter misidentifies `expectTypeOf()` as a floating promise. It is a known false positive, so `typescript/no-floating-promises` is off for `**/*.test.ts` in the `.oxlintrc.json` override rather than repeated as a file-level header (see [tooling.md § oxlint-disable directives live next to the code](./tooling.md#oxlint-disable-directives-live-next-to-the-code)).