@jterrazz/test 16.1.1 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +1 -2980
- package/dist/browser/services.js +1 -0
- package/dist/browser/stubs.js +1 -0
- package/dist/browser-shared-index.browser.js +3233 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +3721 -2624
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +12 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1641 -3004
- package/dist/index.js +5095 -3835
- package/dist/mobile-session.global-setup.d.ts +15 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6069 -5634
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +3979 -5799
- package/dist/route-coverage.global-setup.d.ts +43 -0
- package/dist/route-coverage.global-setup.js +1 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +1 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +34 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +73 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-listened-port.js +19 -0
- package/dist/shared-literate-plugin.js +184 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +22 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +328 -0
- package/dist/shared-process-alive.js +13 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-record-key.js +13 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +407 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +174 -0
- package/dist/shared-session-ledger.js +305 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +303 -0
- package/dist/shared-spec-grammar.js +66 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-specs-modules.js +1033 -0
- package/dist/shared-stub-reply.js +167 -0
- package/dist/shared-stub-server.js +142 -0
- package/dist/shared-temp-root.global-setup.js +73 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +306 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +1 -0
- package/dist/vitest.d.ts +93 -164
- package/dist/vitest.js +382 -550
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +566 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +97 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +44 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +500 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +228 -0
- package/package.json +56 -36
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -438
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -282
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -248
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -332
- package/docs/19-linting.md +0 -512
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# Linting
|
|
2
|
+
|
|
3
|
+
The conventions of [Conventions](../concepts/conventions.md) are enforced by three tools, all switched on by the one `test()` preset ([Set up a project](../start/set-up-a-project.md#turning-the-conventions-on)):
|
|
4
|
+
|
|
5
|
+
- **an oxlint plugin**, `@jterrazz/test/oxlint`: the rules that read one file's code;
|
|
6
|
+
- **import and naming rules** the preset hands to `@jterrazz/typescript`'s dependency-cruiser and ls-lint passes: who may import whom, where a file may sit;
|
|
7
|
+
- **the conventions checker**, the checks the `test()` preset brings to `typescript check`: what oxlint cannot read: fixture files, spec documents, several files at once, a whole workspace member.
|
|
8
|
+
|
|
9
|
+
`npx typescript check` runs all three. Each finding is one lesson in four parts and ends on the catalogue row that owns the rule:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
specs/api/users/users.spec.ts:7:5: error jterrazz-check(no-reach-into-sibling-ground): found: "../orders/__seeds__/one.sql" leaves specs/api/users · why: A path leaves its leaf only by climbing: into the ground of a folder that encloses it, the specs root's included, … · fix: move what it names beside specs/api/users, or … · read: docs/reference/linting.md#no-reach-into-sibling-ground
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This page opens on what a project does with a finding (look the rule up, deviate from it, suppress one case), then the generated catalogue of every rule, and ends on how the preset is wired and how each tool keeps its debt, which a project rarely needs.
|
|
16
|
+
|
|
17
|
+
## Quick reference
|
|
18
|
+
|
|
19
|
+
- **Look a rule up.** Every finding's `read:` part is its row: `docs/reference/linting.md#<rule>`, the rule's name as the anchor, in [§ The rule catalogue](#the-rule-catalogue). The row says what the rule holds, the fix and why.
|
|
20
|
+
- **Deviate from a rule** for the whole project, or for a set of files, in `jterrazz.config.ts` under `typescript.oxlint`, with the reason ([§ Deviating from a rule](#deviating-from-a-rule)). The derived `oxlint.config.ts` is never edited.
|
|
21
|
+
- **Suppress one finding** on the line below a directive, with its reason after `--` ([§ Suppressions](#suppressions)):
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// oxlint-disable-next-line jterrazz/runner-in-specification-file -- negative constructor spec: the runner must fail to start
|
|
25
|
+
// checker-disable-next-line database-named-only-when-several -- negative spec: the omitted database is the behaviour under test
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The first form silences a `jterrazz/<rule>` finding of the oxlint plugin; the second a `jterrazz-check(<rule>)` finding of the conventions checker, which oxlint's directives do not reach.
|
|
29
|
+
|
|
30
|
+
## What a finding says
|
|
31
|
+
|
|
32
|
+
Every message a rule prints (a static rule's, a checker pass's, a refusal of the runtime) is one lesson in four parts, in this order: `found:` what was written, located; `why:` the convention, in one sentence; `fix:` the gesture for this very case, with its paths and names; `read:` the row below that owns the rule. The law is [Conventions](../concepts/conventions.md#a-message-teaches); what follows is how each channel prints it.
|
|
33
|
+
|
|
34
|
+
A diagnostic is ONE line, its parts joined by `·`: oxlint's `unix` and `agent` reporters, every editor's problem matcher and the lint specification's report read a finding as one line, and the `unix` reporter prints the rule's name after the message's last line, so a line break inside a message would cut the rule from the finding. The checker prints the same line, as the example at the top of this page shows.
|
|
35
|
+
|
|
36
|
+
A refusal the runtime throws is a block, a part per line: vitest prints an error whole, and what it found (the candidates of an ambiguous element) may take several lines.
|
|
37
|
+
|
|
38
|
+
The `why` and the `read` are never typed: a rule writes its `found` and its `fix` (`lessonsAt()` of `@jterrazz/typescript/rules`, handed the rule's row by name from the consumer catalogue; a finding's two fields in the checker), and the row gives the other two (its sentence's first sentence and its own heading), so a message and the catalogue cannot say two things. This package's own meta-test refuses a message that lacks a part ([Test § The package's own rules](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-packages-own-rules)).
|
|
39
|
+
|
|
40
|
+
## Deviating from a rule
|
|
41
|
+
|
|
42
|
+
`test()` adds this package's catalogue on top of the profile the declared `platform` picks; what each profile adds is [`@jterrazz/typescript`'s own chapter](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/lint-presets.md#the-profiles).
|
|
43
|
+
|
|
44
|
+
A rule set apart is written in `jterrazz.config.ts`, under `typescript.oxlint`, as `{ level, reason }`: the level oxlint reads, options included, and the sentence that says why. The derived `oxlint.config.ts` reads it from there. In the `jterrazz.config.ts` of [Set up a project](../start/set-up-a-project.md#the-vitest-config), imports unchanged:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
kind: 'api',
|
|
49
|
+
platform: 'node',
|
|
50
|
+
presets: [test()],
|
|
51
|
+
typescript: {
|
|
52
|
+
oxlint: {
|
|
53
|
+
rules: {
|
|
54
|
+
'jterrazz/no-probe-cluster': {
|
|
55
|
+
level: ['error', { threshold: 6 }],
|
|
56
|
+
reason: 'a stub table is read field by field',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A set of files judged apart is an entry of `typescript.oxlint.overrides`, `{ files, reason, rules }`. A rule written without its reason, or an override without its files or its reason, is refused in four parts; the toolchain owns the form and the merge order ([its Configuration § The lint rules](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/configuration.md#the-lint-rules)).
|
|
65
|
+
|
|
66
|
+
A rule that takes options is declared, not suppressed: an `off` loses the check everywhere, a raised threshold keeps it. Three rules take one: `specs-at-declared-depth` (`depth`; the four tree shapes are [Conventions](../concepts/conventions.md#the-folder-follows-the-assets)), `no-probe-cluster` (`threshold`, default 3) and `no-module-mock` (`modules`, the specifiers this repository doubles, each `{ module, reason }`; an entry with no reason clears nothing).
|
|
67
|
+
|
|
68
|
+
Rules are addressed as `jterrazz/<rule>`; prefer an override scoped to the files that need it over turning a rule off for the whole project.
|
|
69
|
+
|
|
70
|
+
## Suppressions
|
|
71
|
+
|
|
72
|
+
Standard oxlint disable comments work, and every suppression must carry its reason:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// oxlint-disable-next-line jterrazz/runner-in-specification-file -- negative constructor spec: the runner must fail to start
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The framework's own repo keeps its sanctioned exemptions visible, as `typescript.oxlint.overrides` in its `jterrazz.config.ts`, each with its reason: one for the constructor unit tests (`src/**/*.test.ts` may call `specification.*`, testing the constructors is their purpose), and one turning `no-test-code-in-production` off by path for every module whose job is the runner coupling or a test package ([Architecture § The source trees](https://github.com/jterrazz/package-test/blob/main/docs/concepts/architecture.md#the-source-trees)) names them and says why.
|
|
79
|
+
|
|
80
|
+
The **cross-file checker passes** carry their own suppression comment (oxlint disable comments don't reach them; they are the checker's, not oxlint's):
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// checker-disable-next-line database-named-only-when-several -- negative spec: the omitted database is the behaviour under test (runtime channel)
|
|
84
|
+
expect(() => api.seed(file('./__seeds__/one-user.sql'))).toThrow(/* ... */);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`// checker-disable-next-line <name>[,<name>] -- reason` suppresses the named checker passes (any pass's name, or `*`) on the next non-blank line; `// checker-disable-line <name>` suppresses its own line. The text after `--` is stripped before the ids are matched, so it never affects which pass is silenced; `checker-suppression-states-a-reason` requires it on every directive, and `checker-suppression-names-a-pass` refuses an id no pass has. The idiomatic use is a negative spec that deliberately violates a convention to exercise the runtime channel.
|
|
88
|
+
|
|
89
|
+
## What oxlint's own `vitest` plugin owns
|
|
90
|
+
|
|
91
|
+
This catalogue holds only what no upstream rule already holds. Oxlint ships a `vitest` plugin, and `@jterrazz/typescript` turns it on over the test globs in every profile, so a consumer listing `test()` gets both layers and each convention is enforced exactly once.
|
|
92
|
+
|
|
93
|
+
Four hygiene rules are that plugin's: no committed `.only`/`.skip` (`vitest/no-focused-tests`, `vitest/no-disabled-tests`), an `expect()` in every test (`vitest/expect-expect`), no two literal titles alike in a file (`vitest/no-identical-title`), a lowercase title (`vitest/prefer-lowercase-title`).
|
|
94
|
+
|
|
95
|
+
`prefer-lowercase-title` flags a title whose first word is an all-caps identifier (`VALID_CATEGORIES ...`, `HTTP ...`, `DI ...`); the rulebook sets no `allowedPrefixes`: a title starts lowercase, whatever its first word. A title that opens on an identifier is RENAMED (`the VALID_CATEGORIES table ...` reads as the sentence it always was), and a consumer who disagrees sets the rule apart with its reason; the form is [§ Deviating from a rule](#deviating-from-a-rule) above, and what a project may deviate on is [`@jterrazz/typescript`'s own chapter](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/lint-presets.md#the-profiles).
|
|
96
|
+
|
|
97
|
+
## Writing the Given and Then markers under `typescript fix`
|
|
98
|
+
|
|
99
|
+
`given-and-then-markers-present` asks every test for a `// Given -` line and a `// Then -` line, in that order. A marker is a comment, and two rules of the base preset rewrite comments when the fixer runs, and either can turn a well-placed marker into something that means nothing, with the lint green afterwards, because the marker text is still in the file. Whether either rule is on, and in which mode, is `@jterrazz/typescript`'s rulebook to state; what they do to a marker is this page's.
|
|
100
|
+
|
|
101
|
+
**A marker goes between STATEMENTS, never between two declarators of one chain.** `one-var` exists in oxlint from **1.83** (1.74 answers `Rule 'one-var' not found in plugin 'eslint'`), and only its `always` mode arms the hazard. Asked for one `const` statement per scope, the fixer satisfies it by FUSING adjacent declarations, swallowing whatever comment stood between them into the chain, where it now separates two declarators instead of two steps of the test:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Given - the seeded row
|
|
105
|
+
const before = await cli.exec('seed');
|
|
106
|
+
// Then - the row the command wrote
|
|
107
|
+
const after = await cli.exec('read');
|
|
108
|
+
|
|
109
|
+
// After `typescript fix`: the marker has been pulled inside a declaration chain
|
|
110
|
+
const before = await cli.exec('seed'),
|
|
111
|
+
// Then - the row the command wrote
|
|
112
|
+
after = await cli.exec('read');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`given-and-then-markers-present` still passes on the second form, which is what makes it worth knowing: nothing fails, the narration has simply stopped separating anything. The rule's `never` mode is the opposite hazard-free shape: one declarator per statement, so its fixer splits chains and never moves a comment.
|
|
116
|
+
|
|
117
|
+
**A marker is EXACTLY one line.** `capitalized-comments` capitalises the first word of every `//` line, a wrapped continuation included, so a marker spilling onto a second line is capitalised mid-sentence:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Given - a runner primed with a fixture and a seeded database,
|
|
121
|
+
// and the command line that asks for the second shop
|
|
122
|
+
|
|
123
|
+
// After `typescript fix`
|
|
124
|
+
// Given - a runner primed with a fixture and a seeded database,
|
|
125
|
+
// And the command line that asks for the second shop
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`@jterrazz/typescript` v10 turns that rule off (198 of 198 hits on real estate code were false positives on wrapped `//` prose), so the rewrite stops happening under the preset. The one-line discipline outlives it anyway: a marker is the title of a step, and reasoning that does not fit on one line belongs in a docblock above the test, never in the marker.
|
|
129
|
+
|
|
130
|
+
## The rule catalogue
|
|
131
|
+
|
|
132
|
+
The catalogue below is **generated** from the rows of `lint/consumer/catalogue/manifest.json`, which `lint/consumer/catalogue/manifest.ts` reads, where each rule carries its normative text beside its implementation. Never edit it by hand; `npm run docs` regenerates it, and a meta-test fails when the committed table is not what the code says. It lists every `consumer` rule (what a project using the package meets) across the channels a consumer meets (static, the oxlint plugin's rules, then toolchain, upstream, checker, runtime, type, process), grouped by convention family; the meta channel holds only this package's own suite. A `self` rule holds only this package's own suite, and is listed where the package says how it tests itself: [Test § The package's own rules](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-packages-own-rules).
|
|
133
|
+
|
|
134
|
+
A rule answers to ONE name, the same everywhere: the id you address in a config (`jterrazz/<name>`), the code a checker finding prints (`jterrazz-check(<name>)`, keyed `jterrazz-check/<name>` by [the ratchet](#the-ratchet-the-test-section-of-jterrazzbaselinejson)), the name a suppression gives, and the anchor a message's `read:` part lands on (`docs/reference/linting.md#<name>`). The name states the requirement: a ban reads `no-<forbidden thing>`, a requirement names its subject and what holds of it (`seeds-are-sql`, `result-compared-whole`), a quantity reads `one-<thing>-per-<scope>`. A convention held over two channels is two rows, and so two names: the runtime twin alone carries `-at-runtime`. A name carries no severity, and neither does a row: every rule errors.
|
|
135
|
+
|
|
136
|
+
Seven columns. **Rule** is the name, and carries the row's anchor. **Specification** is which constructor's tree the rule is about: `model` for every specification. **Channel** is which of the eight enforces it. **Reach** is which files it looks at, in the vocabulary of `roleOf` (`lint/kit/project/role.ts`): `module` a `*.test.ts`, `component` a `*.test.tsx`, `specification` a `*.specification.ts`, `config` a `vitest.config.*`, `ground` a `fixtures`/`expected`/`requests`/`seeds` tree, plus the two shapes that are not a role: `tests` (any file a test conventions bind) and `all`. Every rule reads a path BELOW the project root that holds the file, never its absolute segments: the nearest package root, or, for a tree no package holds, the repository root (its `.git`): a checkout under a folder named `__fixtures__/`, `stubs/`, `src/`, `specs/` or `__tests__/` answers for nothing in the project it holds. **Convention** is the normative sentence, **Fix** the one imperative line that resolves it, **Rationale** why the rule exists. A `—` in Reach or Fix is a row that states none.
|
|
137
|
+
|
|
138
|
+
The catalogue is grouped by **family**, a word naming what the rules judge (the sections below, generated from `lint/consumer/catalogue/families.ts`), and a static rule's implementation sits in the folder of its family, `lint/consumer/rules/<family>/<name>.ts`.
|
|
139
|
+
|
|
140
|
+
<!-- GENERATED:catalogue. Do not edit by hand; run `npm run docs`. Source: lint/consumer/catalogue/manifest.json -->
|
|
141
|
+
|
|
142
|
+
Every rule a project using the framework meets, across its enforcement channels (**static**: 55 (oxlint rules), **upstream**: 3 (options set on an upstream rule), **toolchain**: 7 (the preset @jterrazz/typescript runs), **checker**: 36 (bundled passes), **runtime**: 15 (execution-time refusals), **type**: 10 (what the compiler refuses), **process**: 2 (review-borne rules)), sourced from one manifest so the code and the catalogue can never drift.
|
|
143
|
+
|
|
144
|
+
## setup: Constructors, configuration, projects and infrastructure
|
|
145
|
+
|
|
146
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
147
|
+
| ----------------------------------------------------------------------------------------------------- | ------------- | ------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| <a id="cleanup-passed-to-after-all"></a>`cleanup-passed-to-after-all` | model | static | specification | The specification file passes `cleanup` to `afterAll`; a `cleanup` destructured but never handed over is an error. | Add `afterAll(cleanup)` at the end of the specification file. | Guaranteeing teardown stops containers and connections leaking between files. |
|
|
149
|
+
| <a id="include-points-at-a-folder"></a>`include-points-at-a-folder` | model | static | config | An `include` glob of a PROJECT (an object under `test` or in `projects`, never `optimizeDeps.include` and its kin) whose static prefix (the segments before the first wildcard, brace or bracket), resolved from the config, is not a directory is an error. | Point the glob at the folder that exists, or delete the project that collects nothing. | A project that collects nothing passes: the suite never ran, and the only trace is a zero nobody reads. |
|
|
150
|
+
| <a id="literate-runner-exists"></a>`literate-runner-exists` | cli | static | config | `literate.runner`, resolved from the config directory, names a file that exists. The config directory is what the path is read against; a project that moves Vite's `root` states its door relative to the config all the same. | Point it at the `*.specification.ts` that exports the `cli` its documents run through. | A path that resolves to nothing fails at collection time with a message about an import, several layers from the line that caused it. |
|
|
151
|
+
| <a id="no-dom-in-module-test"></a>`no-dom-in-module-test` | component | static | module | A module test touches no DOM global: `document`, `window`, `navigator`, `HTMLElement` referenced in VALUE position in a `module`-role file OUTSIDE `specs/` is an error. A name in type position (`x as HTMLElement`) and a `typeof window` guard reach no document and stay out of reach. | A rendered thing is a `.test.tsx` in the `__tests__/` of its component's folder, collected by `component()`. | A module test runs under node, where `document` does not exist: a test asking for one is not testing a module, it is rendering something, and it now has somewhere to go. |
|
|
152
|
+
| <a id="no-raw-env-write"></a>`no-raw-env-write` | model | static | tests | A test writes the process environment through `vi.stubEnv` alone: a raw assignment onto a variable of it (named or computed), a replacement of `process.env` itself, and a `delete` of one of its variables are each an error; a `*.specification.ts(x)` is out of reach by role. | Use `vi.stubEnv('X', 'y')`, which restores itself; a CLI default belongs to `specification.cli({ defaults })`. | The assignment outlasts the test: the next file in the same worker inherits it, and the failure lands somewhere else. |
|
|
153
|
+
| <a id="no-redundant-root"></a>`no-redundant-root` | model | static | all | A `root` pointing at the directory the automatic walk would have found is redundant, and an error. | Drop the option: the walk finds the nearest `package.json` by itself. | Detection by convention has to stay the default; an explicit `root` is only justified where it fails. |
|
|
154
|
+
| <a id="no-runner-alias"></a>`no-runner-alias` | model | static | all | The return is destructured under the constructor's canonical name, unaliased (`{ api, cleanup, docker }`); renaming (`{ api: myApi }`) is an error. | Drop the alias and keep the canonical name. | One instance name per specification makes every spec readable with no local context. |
|
|
155
|
+
| <a id="no-simulated-dom"></a>`no-simulated-dom` | component | static | tests | A test file declares no simulated DOM: the `@vitest-environment happy-dom\|jsdom` pragma is an error. Reach: the roles that RUN a test (`module`, `component`, `specification`); a project config is `no-simulated-dom-in-config`'s. | A rendered thing is a `.test.tsx` in the `__tests__/` of its component's folder, collected by the `component()` project. | A simulated DOM behaves ALMOST like a browser, and what ships is judged by a real one: the component specification renders in the Chromium the website specification already drives. |
|
|
156
|
+
| <a id="no-unknown-constructor"></a>`no-unknown-constructor` | model | static | all | Seven constructors and only seven: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()`, `specification.lint()`. Any other member (`.app`, `.http`, `.stack`...) is an error. A rendered component starts nothing: it is a chain, not a member of this record. | Name one of the seven, or reach for the `component` chain: an eighth subject is a decision before it is a constructor. | A closed surface stops parallel constructors being invented and keeps the API memorable. |
|
|
157
|
+
| <a id="runner-in-specification-file"></a>`runner-in-specification-file` | model | static | tests | A runner is created only in a `*.specification.ts(x)` file under `specs/`: calling `specification.*` anywhere else is an error. The `.tsx` spelling is the same file with a `wrap` written in JSX. A `component`-role file is out of reach: a rendered unit starts nothing. | Move the call into `specs/<specification>/<specification>.specification.ts` and export the handle from there. | Anchoring runners to a recognisable filename makes the entry point discoverable and keeps tests declarative. |
|
|
158
|
+
| <a id="database-named-only-when-several"></a>`database-named-only-when-several` | model | checker | tests | With ≥ 2 databases, `database:` is required on every `.seed()`/`.table()`; with exactly one it is forbidden, which is checked by crossing the `services:` record with the calls in the tests. | State `database:` everywhere or nowhere, whichever the record calls for. | The number of databases fixes the call API; the cross-file analysis catches the omission before run time. |
|
|
159
|
+
| <a id="docker-exec-bound-with-await-using"></a>`docker-exec-bound-with-await-using` | model | checker | tests | Docker-aware runners are inferred from the `docker:` option of the imported specification file, and every `.exec()` result bound without `await using` is reported. | Bind the result with `await using`, so the containers the run spawned are disposed with the scope. | Inferring the runners removes the hand-maintained list an oxlint rule would need. |
|
|
160
|
+
| <a id="document-env-and-serve-registered"></a>`document-env-and-serve-registered` | cli | checker | document | A document's bare `env:` words and `serve:` names are keys of the `env`/`serve` objects of a `specification.cli(...)` in the nearest directory carrying `*.specification.ts` files (literals read statically; ignored otherwise). | Register the name in the specification, or fix the spelling. | A typo (`serve: dashbord`) fails at run time today, a test file later; here it is one lint line. |
|
|
161
|
+
| <a id="no-simulated-dom-in-config"></a>`no-simulated-dom-in-config` | component | checker | member | `environment: 'happy-dom'\|'jsdom'` in a test project of `jterrazz.config.ts` is an error. The MEMBER pass is the one channel that holds it: a settings file is not always a file the repository's oxlint configuration collects, while the member pass starts from the root manifest and reaches every declared member, and one finding is one line in one report. | Remove `environment` and collect the rendered tests with `component()`. | The static pass sees only what the repository's oxlint configuration hands it; the member pass starts from the root manifest and reaches every declared member. |
|
|
162
|
+
| <a id="state-isolated-per-worker"></a>`state-isolated-per-worker` | model | runtime | tests | Isolation is per WORKER: each worker takes its own schema, database index or file copy, so two workers never share state. | Take the handle the runner gives the chain; a client opened at the default index talks to another worker. | Parallelism is the default, and a shared database turns it into a source of failures that never reproduce alone. |
|
|
163
|
+
| <a id="containers-removed-with-the-run"></a>`containers-removed-with-the-run` | model | runtime | tests | The capture is docker-aware: every container a run spawned carries that run label, and the scope exit removes them whether the test asked about them or not. | Bind the result with `await using`, and declare `docker` on the runner. | A container that outlives its run is a machine filling up quietly, and the next run inherits its state. |
|
|
164
|
+
| <a id="database-named-only-when-several-at-runtime"></a>`database-named-only-when-several-at-runtime` | model | runtime | tests | With two SQL databases or more, a seed or a table names the `database:` it reads, and with exactly one it names none. The framework throws at run time if `database:` is absent with ≥ 2 databases, or present with exactly one (it doubles the checker channel). | State `database:` everywhere or nowhere, whichever the record calls for. | The runtime keeps the guarantee even where static analysis abstains. |
|
|
165
|
+
| <a id="seam-is-an-optional-peer"></a>`seam-is-an-optional-peer` | model | runtime | tests | Every seam is an optional peer of @jterrazz/test, installed by the project that specifies through it. A page, a screen, a component and each service load theirs lazily, the first time a spec needs it, and a missing one is refused where it was needed, naming the peer and the install command (the package manager the project's lockfile names, for a service: [Set up a project](../start/set-up-a-project.md#what-to-install)). | Install the peer the refusal names, with the command it gives. | A project that specifies no page, screen or database installs no browser, no simulator driver and no database client: the cost of a seam is paid by the project that uses it, and the refusal names it at the moment it is needed. |
|
|
166
|
+
| <a id="no-server-with-url"></a>`no-server-with-url` | model | type | specification | `server` and `url` are mutually exclusive on `specification.website()`: the pair is inexpressible, not runtime-checked. | State `server` for a site this run starts, `url` for one already running. | They name two different subjects, and a specification that states both leaves the reader to guess which site the spec is about. |
|
|
167
|
+
| <a id="server-sees-declared-services"></a>`server-sees-declared-services` | model | type | specification | The `services` record types the factory argument: `server: (services) => ...` sees exactly the keys the record declares. | Declare the service in the record, or read the key that is there. | A service read by a name nothing started is a startup failure with no line to blame; the compiler has the record in hand and can say so at the call. |
|
|
168
|
+
| <a id="test-settings-section"></a>`test-settings-section` | model | type | config | `jterrazz.config.ts` takes a `test` section, typed by `@jterrazz/test`: `projects`, how a vitest project departs from the one the tree derives, keyed by its specification with its `reason`; `routes`, a path to an OpenAPI document; `collections`, a list of route segments; `basePath`, the path every api route is mounted under, written from the root with no slash at its end (`'/api'`): any other string is refused when the section is read; `methodless`, the paths of routes taken on any method, each with its reason; `production.allow`. A key the package does not read is refused, and so is a list written as one word, a specification the package does not ship, and a project stating no reason. | Write only the keys `@jterrazz/test` reads, lists as lists: `test: { collections: ['data'] }`. | A key nothing reads is a setting that silently does nothing, and the compiler is the one reader that sees the whole file before a run does. |
|
|
169
|
+
|
|
170
|
+
## chain: The chain and its markers
|
|
171
|
+
|
|
172
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
173
|
+
| ------------------------------------------------------------------------------------------- | ------------- | ------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
174
|
+
| <a id="cli-runs-the-product-binary"></a>`cli-runs-the-product-binary` | cli | static | specification | A `specification.cli(bin)` whose binary resolves inside a dependency's `node_modules/.bin` tests the third-party tool, not the product command, and is an error (a suppression with a reason is accepted). Reach: a specification file; a runner created anywhere else is `runner-in-specification-file`'s. | Point the runner at the product's own binary; compare its output whole with `toMatch`, and cut a block with `result.stdout.grep(...)` only where the rest is noise. | A spec has to exercise the product's real command, and asserts on what that command prints. |
|
|
175
|
+
| <a id="given-and-then-markers-present"></a>`given-and-then-markers-present` | model | static | tests | Every test carries `// Given -` then `// Then -` (both, in that order); a Given declared after a Then is an error. A `test.each` table is judged ONCE, on the table, not once per row. A `<case>.spec.yaml` document has no comments: its narration is its `description:`. | Write the two markers, in order, as sentences about the subject, not about the code (Conventions § The narration). | Given/Then narration makes a test's intent readable without reading its assertions. |
|
|
176
|
+
| <a id="given-in-the-test"></a>`given-in-the-test` | model | static | tests | A `beforeEach`/`beforeAll` in a test file is an error: the Given is built in the test. An `afterEach`/`afterAll` gives back what the tests took (a temp directory removed, a server stopped, a mock restored) and stands. | Call a function from each test instead of a hook; `clock.at()` and `intercept()` restore themselves. | A hook moves the setup out of the only place a reader looks, shares it with every test in the file, and half of them come to depend on it quietly. |
|
|
177
|
+
| <a id="marker-on-one-line"></a>`marker-on-one-line` | model | static | tests | A marker is exactly one line: a `//` comment directly under one, at the same indentation, is a wrapped marker and an error. A tool's directive is not a sentence: a linter's `-disable`/`-enable`, a formatter's `-ignore`, a `@ts-` pragma and a coverage line stand where they are. | Fold the continuation into the sentence, or separate it with a blank line. | The narration is a sentence about the subject; a paragraph hides its second half from every reader that only looks at the marker line. |
|
|
178
|
+
| <a id="no-redundant-service-url"></a>`no-redundant-service-url` | cli | static | tests | `.env({ <SERVICE>_URL: <service>.connectionString })` repeats the framework's own injection, and is an error. | Drop the line: the record key already injects `<KEY>_URL` into the child process. | The injection already covers service URLs; rewriting them by hand invites drift. |
|
|
179
|
+
| <a id="no-repeated-chain-setting"></a>`no-repeated-chain-setting` | model | static | spec | A setting every chain of a spec file states alike (the same `.clock(...)`, `.headers(...)` or `.env(...)`, per runner handle) is the file's, stated once on a view of the specification, `const events = api.with({ clock: '...' })`, and is an error at the first chain. A file of one chain is not judged; a seed and a stub are a test's Given, never a setting. | Declare the view once after the imports and the constants it reads, `const <view> = <handle>.with({ ... })`, and start every chain from it: `typescript fix` writes it, named for the file's subject so every file of one folder names it alike (`events` for `events/list/`, its name for a job, its first word for a command; `<subject>View` when the file already holds the name, `<handle>InFile` for no named subject), and drops the setting from each chain. | A setting repeated on every chain reads as a choice each test made; stated once, it reads as what it is (the condition of the whole file) and the next test cannot forget it. |
|
|
180
|
+
| <a id="trigger-name-in-kebab-case"></a>`trigger-name-in-kebab-case` | job | static | tests | `.trigger(name)` takes a stable kebab-case identifier; a non-kebab-case `name` is an error. | Write the job name in kebab-case, the same spelling the application registers it under. | The job name is a contract between the app and its tests: a stable identifier forbids divergent translations. |
|
|
181
|
+
| <a id="when-between-given-and-then"></a>`when-between-given-and-then` | model | static | tests | A `// When -` marker is optional; written, it sits after `// Given -` and before `// Then -`. | Move it to the action it narrates, or drop it when the chain is the action. | The optional marker is the one the order slips on: a `When` above the Given or below the Then tells a story the code does not follow. |
|
|
182
|
+
| <a id="route-written-as-template-at-runtime"></a>`route-written-as-template-at-runtime` | api | runtime | tests | A request written as its route template is handed exactly the parameters the template names. The chain fills each `:name` segment with its value, URI-encoded, the query kept, and sends the filled path; route coverage reads the request by its template, a parameter where the route declares one and each literal segment as itself. A template handed no params, a parameter with no value and a value no parameter takes are refused before a request is sent, naming the call to write. | Hand exactly the template's parameters: `.get('/events/:id', { params: { id } })`. | The compiler holds a typed spec to its template; a cast or a JavaScript spec is held here, before a request the app would answer 404 is taken for a finding about the app. |
|
|
183
|
+
| <a id="sequence-expressed-by-seeding"></a>`sequence-expressed-by-seeding` | model | runtime | tests | A sequence is expressed by SEEDING the state the second run would have found: every chain starts on databases the framework has reset. | Seed the state the earlier step would have left, and open one chain per action. | A spec that depends on a previous one passes alone, passes in order, and fails the day the runner reorders them. |
|
|
184
|
+
| <a id="disk-named-by-relative-path-at-runtime"></a>`disk-named-by-relative-path-at-runtime` | model | runtime | tests | A verb that reads the disk takes a `file()` or a `directory()`, whose path starts with `./` or `../` and ends in `/` exactly when it names a directory. The framework refuses at run time a verb that reads the disk (`seed`, `request`, `fixture`, `run`) handed anything but a `file()`/`directory()` (a bare string names the wrapper to write), and a `file()`, `directory()` or document `fixture:` whose path does not start with `./` or `../`. The trailing `/` says which of the two a path is, and the disk must agree: a directory named without it, or a file named with it, is refused, and so is a path that names nothing, with the path it resolved to. | Name the disk from the spec file: `file('./__seeds__/users.sql')`, `directory('../../__fixtures__/shop/')`. | The type refuses a bare name at the call; the runtime keeps the law for a caller the compiler does not see: a JavaScript spec, a cast, a document. |
|
|
185
|
+
| <a id="service-urls-injected"></a>`service-urls-injected` | cli | runtime | tests | In `cli` mode with `services`, the framework injects `<KEY>_URL` (camel-aware CONSTANT_CASE) plus the unambiguous aliases `DATABASE_URL`/`REDIS_URL`; `.env()` overrides (`null` unsets). | Read the variable the record key injects; state `.env()` only to override or unset it. | Injecting the URLs avoids the repeated hand-wiring and its drift (see `no-redundant-service-url`). |
|
|
186
|
+
| <a id="disk-named-by-relative-path"></a>`disk-named-by-relative-path` | model | type | tests | A string is a value and the disk is `file()` or `directory()`: `seed('./__seeds__/x.sql')` (a bare string where a file is meant), `seed(file('./__seeds__/x.json'))` (the wrong extension), `file('x.sql')` (not a relative path), `directory('./__fixtures__/shop')` (a directory without its `/`), `get(file(...))` (a file where a value is meant) and `toMatch(directory(...))` on a stream do not compile. | Name the disk from the spec file: `file('./__seeds__/users.sql')`, `file('./__expected__/help.txt')`, `directory('../../__fixtures__/shop/')`. | A bare name resolved against a hidden folder is a convention the reader has to know; a relative path is read where it is written, and the compiler refuses the other form at the call. |
|
|
187
|
+
| <a id="no-chain-label"></a>`no-chain-label` | model | type | tests | A chain carries no label: the test name is the sentence. | Name the test; the chain states what it does, not what it is called. | A second place to read the intent is a second place for it to be stale. |
|
|
188
|
+
| <a id="one-terminal-action-per-chain"></a>`one-terminal-action-per-chain` | model | type | tests | Exactly ONE terminal action closes a chain: what it answers is a result, and a result carries no verbs to continue with. | Open a second chain for a second action. | Two actions in one chain make a test with two Whens, and the reader cannot tell which one the Then is about. |
|
|
189
|
+
| <a id="route-written-as-template"></a>`route-written-as-template` | api | type | tests | A request to a route with parameters is written as the route's template, its values beside it, `api.get('/indicators/:symbol/chart', { params: { symbol: 'btc' } })`, and the template types them: every `:name` it holds takes a value, and no other name does. `get` and `delete` take the options second, `post` and `put` third, after the body; a path with no parameter takes none, or `{ params: {} }` to state that it is the route itself. | Write the route as the app declares it and hand its values: `.get('/events/:id', { params: { id } })`. | A filled path hides which of its segments are values: the reader, the subject reader and route coverage all have to guess that `btc` in `/indicators/btc/chart` is a symbol. The template says it, and the compiler holds the values to it. |
|
|
190
|
+
|
|
191
|
+
## layout: Files, folders, sizes, names, and the suppressions a file carries
|
|
192
|
+
|
|
193
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
194
|
+
| --------------------------------------------------------------------------------------------------- | ------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| <a id="golden-path-has-extension"></a>`golden-path-has-extension` | model | static | tests | A golden `file()`'s path carries its extension (`toMatch(file('./__expected__/help.txt'))`, `'./__expected__/<case>.aria.yaml'` for an ARIA tree); a golden file with no extension is an error. A tree golden is `toMatch(directory(...))`, whose path ends in `/`. | Write the fixture's full name, extension included. | The extension is part of the golden's path: the path IS the file, and a file without its extension is another file. |
|
|
196
|
+
| <a id="one-describe-per-file"></a>`one-describe-per-file` | model | static | tests | A test file (a `.spec.ts`, a `.test.ts`, a `.test.tsx`) holds one top-level `describe`, titled with what the file is about, and every test inside it. A spec file's title is its aspect: `pagination.spec.ts` holds `describe('pagination', ...)`. A module test's is its subject, read from the module it names (`<base>.ts`, `<base>.tsx` in the folder above its `__tests__/`, or beside a rendered unit under `specs/`): the name its default export declares; else its one value export; else the value export spelling the base name in camelCase or PascalCase, or the base less its role suffix (`request` for `request.action`); else the base name. So `__tests__/locale-note.test.tsx`, naming `export function LocaleNote`, holds `describe('LocaleNote', ...)`. A title that states the file's subject in other words is that subject: the file's base name read with spaces (`cli reference` for `cli-reference.spec.ts`), or the subject in another spelling (`<LocaleNote />`); a website spec's page or an api spec's route is its subject too (`describe('/articles/:id', ...)` for a spec that visits `/articles/:id`, `'GET /events/:id'` for one that requests it). A `describe.each` names its rows, so its title starts with that name, `'pagination — page %s'`; a subject keeps its case, the `test()` preset exempting the top-level `describe` from `vitest/prefer-lowercase-title` (`ignoreTopLevelDescribe`). A file with no `describe` is refused at its first test, a second `describe` or a test beside the one at itself, a title naming anything else at the title; a file that declares no test through `describe`/`test`/`it` (a rule tester's cases, a type test) is out of reach. | Title the file's one `describe` with its aspect or its subject, or with the file's name in words, and put every test inside it: `typescript fix` wraps a file that has none, moves a test beside the group into it, and merges several groups into one, every test keeping its title and the group titles leaving with their groups, when every group holds tests alone and no two tests share a title; what stands between the tests stays at module scope. It never renames a title: a wrong one, a table's included, is reported and left to its author. | The runner reports a failure under the describe it stands in, so a title that is the aspect or the subject names the file the failure came from; a second group, or a stray test, is a second question the file answers. |
|
|
197
|
+
| <a id="paths-named-in-the-test"></a>`paths-named-in-the-test` | model | static | all | `file()` and `directory()` are called in a test or spec file (a `.test.ts(x)`, a `.spec.ts`, a type test) and nowhere else: a shared helper module, a specification file or a stub takes a `file()` as a parameter and never builds one. The call is recognised by its import from the framework's entry, an alias included. | Take the `file()`/`directory()` as a parameter of the helper, and build it in the test that calls it. | A path resolves against the spec that starts the chain, so one built in a helper resolves per caller: the same line names a different file for every spec that reaches it, and the file it names is visible on no line of the test that stands on it. |
|
|
198
|
+
| <a id="request-file-extension-known"></a>`request-file-extension-known` | api | static | ground | `__requests__/` holds what a spec SENDS: a whole request as a `.http` (`request()`), or a body a verb sends (`post`/`put`/`patch` with a `file()`), whether a `.json`, a `.txt` or any format the app reads. Every file there carries its extension; a file with none is an error. | Name the file with its extension: `.http` for a whole request, the body's own format otherwise. | The extension is how the file is sent (a `.http` whole, a `.json` parsed, anything else verbatim), so a file without one says nothing about what the app will receive. |
|
|
199
|
+
| <a id="spec-file-within-line-limit"></a>`spec-file-within-line-limit` | model | static | spec | A spec file holds at most 400 lines; a longer one is refused at its first line, the message grouping its tests by the prefix of their titles, where the cut usually goes. A file already past it is recorded on the ratchet and may only shrink. | Cut the file along the groups the message names, one aspect per file, in the same folder. | A file past a few hundred lines asks several questions of its subject, and a reader looking for one scrolls through the others; a cap written as a number is the one a reviewer does not have to argue. |
|
|
200
|
+
| <a id="spec-named-for-its-aspect"></a>`spec-named-for-its-aspect` | model | static | spec | A spec file is named for its aspect: kebab-case (`not-found.spec.ts`), and never a name that says the file holds what fit nowhere else (`misc`, `other`, `basic`, `general`, `more`, `edge-cases`, `tests`, `main`). | Rename the file for the question its tests ask of the subject: `pagination`, `filters`, `sorting`, `language`, `validation`, `not-found`, `auth`, `shape` are the api vocabulary the Conventions page advises. | The folder names the subject; the file name is the only place the question asked of it is written, and a catch-all name answers none: the next test lands there because it fits nowhere, and the file grows until it holds everything. |
|
|
201
|
+
| <a id="specs-at-declared-depth"></a>`specs-at-declared-depth` | model | static | specs | A specs tree DECLARES its shape through the `depth` option: `specification-domain` (the default, a test at specification/domain depth), `specification` (a test at the specification root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. In every mode, `off` included, a `*.specification.ts` sits at the ROOT of its specification, the one place a runner lives. Under `specification-domain` and `specification`, a `.spec.ts` of the api or cli specification sits as deep as its subject's standard name (a route's resource and action, `events/list/`, a command's words, `db/migrate/`), which `spec-in-subject-folder` judges. A first-level folder that is no specification (a repository suite, `surface/` or `lint/`) is judged by the same depth as a specification is. In every mode, `off` included, a directory named `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` is GROUND, and `__setup__/` holds the code that prepares specs: no spec lives inside either. One exception, and only one: a special folder may hold CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there, since a specs tree holds no `__tests__/`. A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A first-level folder is a specification, a special folder (what the specifications share) or a repository suite, which holds tests of its own (one test is enough). A module in one that is none of the three (a `support/` of helpers and mocks), a runner included, is refused in every mode, `off` included: a runner lives at the root of the specification it builds, a test helper in the `__setup__/` of the lowest folder its tests share, a fixture under `specs/__fixtures__/`. A `.test.tsx` is out of reach: a rendered unit is specified in the `__tests__/` of its component's folder, never in a specification/domain tree, and `test-in-its-tests-folder` holds its neighbourhood. | Move the file to the depth the tree declares, or declare the depth the tree actually has; move a runner to its specification root, and a helper into the `__setup__/` of the lowest folder its tests share. | A fixed depth makes every file's place predictable and statically checkable. |
|
|
202
|
+
| <a id="test-body-within-line-limit"></a>`test-body-within-line-limit` | model | static | tests | A test body spans at most 60 lines of gestures, counted over the function a `test(...)`/`it(...)` runs from the line it opens on to the line it closes on, blank lines and comments included, less the lines the expected value of a `toMatch`, `toStrictEqual` or `toEqual` holds past the one it opens on (that value is the claim the test makes, and `ground-shared-or-inline` caps a `toMatch` one, not this rule); a longer one is refused at the function. A body already past it is recorded on the ratchet and may only shrink. | Split it into one test per behaviour, or move a long Given into a function the test calls. | A test past a screen asks several things of its subject, and the one that fails is the one a reader has to find; the toolchain's function cap leaves a test file alone, because a file's one `describe` is a function as long as the file, so this cap is the test's own. |
|
|
203
|
+
| <a id="test-in-its-tests-folder"></a>`test-in-its-tests-folder` | model | static | tests | The test of `<dir>/<file>.ts` is `<dir>/__tests__/<file>.test.ts`: the conventional folder of its module's folder, holding its tests, the typed data they read (`<file>.fixtures.ts`) and the ground they stand on. A `*.fixtures.ts` outside `specs/` is test material and sits in a `__tests__/` too: its module's, or (when the module tests of several folders share it) the one of the lowest folder they share, as a `__setup__/` does under `specs/`; anywhere else it is an error. A module test outside a `__tests__/` is an error, the fix naming the folder it moves to; one in a `__tests__/` naming no module of the folder above it, and a package's `tests/` or `test/` root, are errors. A RENDERED unit answers to the same law: a `.test.tsx` names the `.tsx` it renders, or the `.ts` of the hook or DOM function whose Host it carries. A specs tree holds no `__tests__/`: a rendered unit under `specs/` keeps its component beside it, and a `module`-role file there is a repository suite: it covers a tree, so it names no module. | Move the test into the `__tests__/` of the folder of the unit it covers, under the same basename, and each relative path it imports one folder up; a `*.fixtures.ts` into the `__tests__/` of the lowest folder its readers share. | Neighbouring tests (parity with Go's `foo_test.go`) keep a test and its code together and discoverable. |
|
|
204
|
+
| <a id="verb-reads-its-ground-folder"></a>`verb-reads-its-ground-folder` | model | static | tests | The verb and the ground folder agree: `seed()` reads `__seeds__/`, `request()` and a request body (`post`/`put`) read `__requests__/`, `fixture()` reads `__fixtures__/`, a golden `toMatch(file\|directory(...))` reads `__expected__/`, and `run()` reads a document beside the test, under no ground folder, or under `__fixtures__/` for a document that is an input a negative spec runs. The same four folders at the specs root hold what several specifications share, so a shared seed is `<specs>/__seeds__/x.sql` (`../../__seeds__/x.sql`), never `<specs>/__fixtures__/__seeds__/x.sql`: the role folder is the first ground folder the path passes through, and it is the one judged. The first ground folder a literal path passes through is the one judged, whether the `file()`/`directory()` is handed to the verb inline or bound first to a `const` of the same file (the nearest declaration of the name, as the language resolves it); a `let`, a parameter or a computed path is out of reach, and a document's `fixture:` is held to `__fixtures__/` by its grammar (`document-follows-its-grammar`). | Move the file into the folder its verb reads, the leaf's own or that of the lowest folder the leaves share (the specs root's when they span specifications), and spell the path through it. | Four reserved names are a vocabulary only if each means one thing: a seed parked in `__fixtures__/` or a golden in `__requests__/` makes the folder lie about what it holds, and the next reader trusts the folder. |
|
|
205
|
+
| <a id="one-describe-level"></a>`one-describe-level` | model | upstream | tests | At most one level of `describe`: `vitest/max-nested-describe` with `{ max: 1 }`, set by the `test()` preset. | Flatten the nesting: the test name is the sentence, and one level groups the file. | A tree of contexts is state a reader has to hold in their head to know what a test is about, and the name of the test stops being the sentence it was. |
|
|
206
|
+
| <a id="spec-file-in-a-specs-tree"></a>`spec-file-in-a-specs-tree` | model | toolchain | all | A `.spec.ts` (the ASSEMBLED product) sits under a `specs/` tree; one in any other folder is an error at the file. One at a project's own root is `file-suffix-matches-place`'s. | Move it under `specs/<specification>/`, or rename it `.test.ts` in the `__tests__/` of the folder of the module it covers. | The suffix says the kind: a `.spec.ts` outside the specs tree is a spec nobody running the specifications finds, or a unit test wearing the wrong name. |
|
|
207
|
+
| <a id="special-folder-in-closed-list"></a>`special-folder-in-closed-list` | model | toolchain | specs | A folder under a specs tree whose name starts with `_` is one of the closed list of special folders (`__setup__/`, `__expected__/`, `__fixtures__/`, `__requests__/`, `__seeds__/`) and any other `_` name is an error at the folder, the closed list in its fix: an unknown `__<word>__/` (`__mocks__/`, `__snapshots__/`, `__helpers__/`, `__tests__/`), a subject that starts with `_`, and an earlier spelling (`_setup/`, `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`, whose fix names its special folder, and `_shared/`, whose folders sit at the specs root now). What sits inside a `__setup__/` is `reserved-folder-in-closed-list`'s, and ground is never entered. | Rename the folder to the special folder that says what it holds (an earlier `_<name>/` to its `__<name>__/`) and the paths that name it; move what a `_shared/` held into the special folders at the specs root; drop the `_` from a subject. | A route, a command or a job names its subject folder, so a subject can take any word: only a marker keeps the folders that are no subject from colliding with one, and only a closed list keeps the marker meaning something, and the marker is the `__<word>__/` the ecosystem already reads as no route (Jest's `__tests__/`, `__mocks__/`). |
|
|
208
|
+
| <a id="specs-root-holds-specifications-and-shared"></a>`specs-root-holds-specifications-and-shared` | model | toolchain | specs | The specs root holds its specification folders and the special folders of what they share alone (a repository suite is a first-level folder too): code every specification shares is `<specs>/__setup__/`, data the ground at the specs root. A file directly under it that is no test, no spec, no document and no runner is an error, save the four files of a unit (`package.json`, `jterrazz.config.ts`, `tsconfig.json` and `vitest.config.ts`), which a workspace's own suite, a member at `specs/`, keeps. A spec or a runner at the root is `specs-at-declared-depth`'s, and a path into a sibling's ground `no-reach-into-sibling-ground`'s. | Move code into `<specs>/__setup__/` (or the `__setup__/` of the one specification that reads it) and data into the ground folder of its role. | The first listing a reader opens names the specifications: a helper or a data file beside them is a second kind of thing at the one level where every entry is a constructor. |
|
|
209
|
+
| <a id="adapter-named-for-its-port"></a>`adapter-named-for-its-port` | model | checker | specs | A `__setup__/` module whose class `implements` a type it imports from outside the specs tree (the application's port) is a test adapter, and its file is named for that port: `<port>.adapter.ts`, the port's name kebab-cased with a trailing `Port` dropped (`ticker-source.adapter.ts` for `TickerSourcePort`); the class keeps whatever name it has. Where a module's classes implement several ports, any one of their names passes. A double built otherwise (an object literal, a class extending the adapter it replaces) is out of reach, and review names it. | Rename the file `<port>.adapter.ts` for the port its class implements, and the paths that import it. | A test adapter replaces one port of the application, and the port is the name the application gave the seam: named for it, the adapter is found from the port and the port from the adapter, whatever the class is called. |
|
|
210
|
+
| <a id="checker-suppression-names-a-pass"></a>`checker-suppression-names-a-pass` | model | checker | specs | Every id a `checker-disable-next-line` / `checker-disable-line` directive names is a pass of the checker, or `*`. An id no pass has (misspelled, or retired by a major) is an error at the directive. | Name the pass the line is the exception to, or delete the directive. | A directive naming an id no pass has silences nothing, and reads as if it did: a major renames its passes with no bridge, so without this pass every directive naming a retired id would stay in the tree forever, stating an exception nobody holds. |
|
|
211
|
+
| <a id="checker-suppression-states-a-reason"></a>`checker-suppression-states-a-reason` | model | checker | specs | Every `checker-disable-next-line` / `checker-disable-line` directive carries `-- <reason>`, wherever a checker pass can be silenced: a test, a document, a fixture under ground. The word inside a string is prose, not a directive. | Write `checker-disable-next-line <name> -- <reason>`. | A suppression with no reason is a rule turned off by someone no longer here: the next reader cannot tell whether the pass was wrong or simply in the way, so the line survives every review. The toolchain's own gate reads `oxlint-disable*` and nothing else. |
|
|
212
|
+
| <a id="document-description-reads-as-a-title"></a>`document-description-reads-as-a-title` | cli | checker | document | A document's `description:` is a title: one line, lowercase initial (an all-caps identifier exempted), no trailing period, under 100 characters. | Write it as a title, not as a sentence. | The description IS the vitest title: a title's rules apply to it, not a paragraph's. |
|
|
213
|
+
| <a id="document-description-unique"></a>`document-description-unique` | cli | checker | document | Two `<case>.spec.yaml` documents in one directory do not share a `description:`. | Name what each case proves; two titles that read alike leave two files to open. | The reporter names the title: two identical titles leave two files to open. |
|
|
214
|
+
| <a id="file-suffix-matches-place"></a>`file-suffix-matches-place` | model | checker | specs | The suffix says the kind, and the tree agrees with it. A `.spec.ts` (the ASSEMBLED product) lives under `specs/<specification>/`: one at a project's own root is an error here, one in any folder `spec-file-in-a-specs-tree`'s. A `.test.ts` (the UNIT) lives in the `__tests__/` of its module's folder; under a specification folder (`api`, `cli`, `integration`, `job`, `lint`, `mobile`, `website`) it is an error, fixable by `typescript fix` (a `git mv`). A `.test.ts` that reaches no runner is `specification-test-uses-its-runner`'s, whose fix is the opposite move, and this rule's fix leaves it alone. A first level that is NOT a specification is a repository suite: it covers a tree, keeps `.test.ts`, and `specs-at-declared-depth`'s declared depth is what judges it, unless the tree carries its `*.specification.ts` at its own ROOT, which makes the whole tree one specification's and every `.test.ts` below it a spec. A document is named `<case>.spec.yaml`: `<case>` in kebab-case, without the words `test`/`spec`/`cli` the suffix already carries, and never the bare name of its directory. | Move the `.spec.ts` under `specs/<specification>/` (or rename it `.test.ts` in the `__tests__/` of its module's folder); rename the specification `.test.ts` to `.spec.ts`: `typescript fix` does it with `git mv`, and names every `include`/`exclude` entry of the member the rename broke. | With no agreement between the suffix and the tree, a reader cannot say what a test proves without opening it; and the filename is the first sentence they read: `rm/rm.spec.yaml` says nothing twice. |
|
|
215
|
+
| <a id="ground-beside-its-only-reader"></a>`ground-beside-its-only-reader` | model | checker | ground | Ground in a leaf holding ≥ 2 node tests (`.test.ts`, `.spec.ts`) that a single one of them reads is an error. A `.test.tsx` tree is out of reach, as it is for `specs-at-declared-depth`, and so are the api, cli and job specifications: there a leaf is its subject's standard name (`spec-in-subject-folder`), its aspect files share it by design, and a folder of one spec's own is one `spec-in-subject-folder` moves the spec back out of. | Give that spec its own domain folder, with the ground beside it. | Ground one spec reads, parked where several sit, reads as shared: everyone assumes someone else depends on it and nobody dares touch it. |
|
|
216
|
+
| <a id="ground-shared-or-inline"></a>`ground-shared-or-inline` | model | checker | ground | A ground file exists only if it is shared, and a value lives in a test only while it stays short. The checker counts the TESTS that read each file of the specs trees, and of the ground a module test stands on inside its `__tests__/` (`src/render/__tests__/__expected__/`), a `test()`/`it()` whose body names it with a literal `file()`/`directory()`, a document, and judges its level. Read by ONE test, named by no module test of the member, and read by no document's `fixture:` line, no import and no path joined to `import.meta.dirname`, a file of `__expected__/` or `__requests__/` is an error where the test names it: it belongs in the test, written in place (a golden as the value `toMatch` compares, a request as its text or its body value) unless it is a directory tree, a binary file, a text no literal survives in place (one opening on a line break whose lines share an indentation, which the comparison's dedent strips from the template and the quoted form alike), or longer than its role's cap: 30 lines for `__requests__/`, 150 for `__expected__/` (a JSON file counted as the value it becomes). A golden compared `{ frozen: true }` stays a file, as the bytes are then the contract; one that is a byte copy of a file the repository keeps is reported to be compared with that source, and the fixer leaves it. The caps hold the other way too: a request or a body written in the test past 30 lines, an expectation past 150, is an error where it is written, and belongs in a file beside the test. `__seeds__/` has no form in place: SQL is never written in a test, and SQL tagged in one is an error where the tag stands. A seed is a `.sql` file, beside its test in `./__seeds__/` when one test reads it, never an error for being unshared. `__fixtures__/` is file state a program reads, never a value, and stays a file. Read by several tests, a file lives in the ground of the LOWEST folder its readers share, the law `setup-module-at-lowest-common-folder` holds code to: beside them when they are one leaf's, in `events/__seeds__/` when `events/list/` and `events/get/` read it, in a route's own when the route and the routes under it read it (`map/pois/` and `map/pois/detail/`), and in the specs root's shared ground (`<specs>/__seeds__/`, ...) only when they meet at the root, across specifications. An entry kept above that folder (a pool entry one specification alone reaches among them) is an error where it sits, and belongs in that folder's ground of the same role, and every reader counts where it stands: a test at its folder, a runner at its specification root, a `__setup__/` module at the folder that owns that `__setup__/`. Only a reader outside the specs tree keeps an entry where it is whatever the specs say: a module test of the member, by a relative literal, the literal pieces `named-path-exists` reads or a whole literal spelled from the member root (`'specs/...'`), and the member's Makefile or vitest config, by a path spelled from that root. `no-reach-into-sibling-ground` refuses the path that leaves a leaf for a sibling's, and `no-duplicate-ground` finds copies. A reference outside every test (a `const` of the file, a specification, a helper) counts as shared, and zero references is `no-unused-ground`'s. Fixed by `typescript fix`: SQL a test holds is written to `./__seeds__/<name>.sql` beside it (named by its first `--` comment, kebab-cased, else by the first four meaningful words of its test's title) and named with `file()`; a value past its cap is written to `./__requests__/` or `./__expected__/` beside its test, named by the test's title, and named with `file()`, never one a plain value is compared to (`expect(message).toMatch(...)`, a name or a string literal as the subject): a string has no golden, and the finding names `text()` to wrap it with; a single-use file whose one reference is a literal `file()` handed to `.request(`, `.post(`/`.put(` or `toMatch(` is written in place and deleted (a `file` import no longer used dropped, an emptied ground folder removed); an entry kept above the lowest folder its readers share is moved into that folder's ground (a `git mv` in a work tree) and the literals rewritten from each referrer. | Run `typescript fix`: it writes SQL and a value past its cap to a file beside the test, writes a single-use file into its one test and deletes it, and moves an entry kept above its readers down to the lowest folder they share. A test whose subject IS the file form keeps it, with a reasoned `checker-disable-next-line ground-shared-or-inline`. | A file a test reads is read at a distance: the reader opens a second file to know what the test stands on or expects, and a file one test reads pays that price for nothing, until the value is too long to read in the test, where the file is the clearer page. SQL is the exception at any length: a seed is database state a reader opens as a file, and SQL in a TypeScript string is checked by nothing. A single scenario's file parked in the pool is worse still: it reads as shared, so nobody dares change it. |
|
|
217
|
+
| <a id="named-path-exists"></a>`named-path-exists` | model | checker | ground | Every `file()` and `directory()` a test names with a literal path (a spec under `specs/` and a module test in its `__tests__/` alike), and every path a document's `fixture:` names, names an entry that exists inside a git work tree, one git does not ignore, of the kind the wrapper says, resolved from the test file's directory: a missing or ignored path, a `file()` naming a directory or a `directory()` naming a file is an error, reported where the path is written. So does a path a test resolves from its own directory with literal pieces alone (`resolve(import.meta.dirname, ...)`, `join(...)` alike, `__dirname` alike, `new URL('...', import.meta.url)`), of either kind: several pieces are read joined (`resolve(import.meta.dirname, '..', '__fixtures__', 'gone')`), one piece only when it is spelled `./` or `../`, as a `file()` is. A lone bare piece (`resolve(import.meta.dirname, '__expected__')`) is an output folder a test makes as often as a path it reads, and is not read. A path under the unit's build output (`dist/`, `.artifacts/<tool>/`) is accepted missing, in a spec and in its specification alike: the kind's test builds before it runs. The tree pass reads the `specs/` trees, the member pass every other test of the member. A computed piece leaves its whole path out of reach, as a computed path is, a path spelled into a command a process runs is the author's, and the runtime refuses it with the path it resolved to; a call written inside a string is text, and is not read. | Create the entry (out of `.gitignore`), or fix the path: the message names what it resolved to. | A typo in a path fails a run, a test later, far from the line that made it; statically it is one report line at the path. |
|
|
218
|
+
| <a id="no-document-in-ground"></a>`no-document-in-ground` | cli | checker | document | A `<case>.spec.yaml` is a SPEC, not ground: a document under `__expected__/`, `__requests__/`, `__seeds__/` or `__fixtures__/` is an error. | Move the document beside the test that runs it. | A document IS the scenario; filed under ground it reads as material a scenario stands on, and the reader cannot tell which files are run. |
|
|
219
|
+
| <a id="no-duplicate-ground"></a>`no-duplicate-ground` | model | checker | ground | Ground several leaves share lives once: an entry of a leaf's ground (a file, or a tree compared file by file) that another leaf holds byte for byte under the same role, whatever either is called, is an error on every copy; it belongs once, in the ground of the lowest folder the leaves share (`events/__seeds__/<name>` for `events/list/` and `events/get/`, `<specs>/__seeds__/<name>` across specifications), named by the path that climbs to it. Fixed by `typescript fix` when the copies share one name: one copy is moved there (a `git mv` in a work tree), the others are deleted, and every whole relative literal that named a copy is rewritten (a literal of the specs tree's sources, a key of its documents, a literal of the member's module tests); a path built from pieces, or spelled into a command, is the author's to rewrite, `named-path-exists` names one built from literal pieces once the copy has moved. Copies of several names, and a ground already holding the name with other content, are left to the author. | Run `typescript fix`, or keep the copies apart on purpose and record them in the ratchet. | Copies drift: one leaf's seed gains a column, the other's does not, and the reader of either cannot tell a difference meant from one forgotten. |
|
|
220
|
+
| <a id="no-reach-into-sibling-ground"></a>`no-reach-into-sibling-ground` | model | checker | ground | A path leaves its leaf only by climbing: into the ground of a folder that encloses it, the specs root's included, whose `<specs>/<role>/` holds what the specifications share. A path a `file()`, a `directory()` or any path key of a document names, whose target is neither under the spec's own directory, nor under one of `<specs>/__fixtures__/`, `<specs>/__seeds__/`, `<specs>/__requests__/`, `<specs>/__expected__/` (the four roles the specs root keeps beside its specifications) nor under the four of a folder above it (`map/pois/detail/` reads `../__seeds__/` of `map/pois/`, and `events/list/` and `events/get/` read `../__seeds__/` of `events/`), is an error; ground shared by several leaves goes to the lowest folder they share, in the folder of its role (`verb-reads-its-ground-folder` judges which, `ground-shared-or-inline` which folder). A leaf's ground reached from a sibling leaf is one such path, so this row holds the sibling case too. | Copy it under this leaf's own ground, or move it to the ground of the lowest folder the leaves share: `<specs>/<role>/` when they span specifications. | A leaf reaching into a neighbour's fixtures is the pool through another door, without the pool's visibility: the neighbour breaks a spec two folders away without knowing. |
|
|
221
|
+
| <a id="no-snapshot-folder"></a>`no-snapshot-folder` | model | checker | all | One word per meaning: a folder named `snapshots/`, `__snapshots__/`, `golden/` or `goldens/` is an error anywhere in a member when a test would stand on it; any such folder under `specs/`, and elsewhere one that holds no module of code (a `goldens/` folder of matcher source is named for what its code does). What it would hold is what a test asserts, and that is `__expected__/`. Ground is not entered: a fixture project may carry the word on purpose. | Rename the folder `__expected__/`, and the paths that name it. | Four reserved names are a vocabulary only while nobody adds a fifth: a second word for the goldens is a second place a reader has to look, and the rules that read `__expected__/` never see it. |
|
|
222
|
+
| <a id="no-unused-ground"></a>`no-unused-ground` | model | checker | ground | No dead fixtures: every file under `__seeds__/`/`__requests__/`/`__fixtures__/` and every top-level entry of `__expected__/` must be referenced: a folder's by the tests at or below it, the specs root's shared four by any test of the tree; a folder holding ground with no test file and no `*.spec.yaml` at or below it is an orphan. The ground inside a module test's `__tests__/` is held the same way: an entry no test of that folder names is dead. A reference is a path RESOLVED to the entry, never a name: a document's keys and a `file()`/`directory()` argument from the file that writes them, the words of any other literal from that file and from the project root, a template literal's table from the folders written before its hole, so `./__seeds__/x.sql` in a leaf vouches for that leaf's `__seeds__/x.sql`, and not for `<specs>/__seeds__/x.sql`. A `<case>.spec.yaml` references the files every path key of it names. | Reference it from the spec it stands under, or delete it. | The `named-path-exists` mirror: a fixture nothing references is dead weight that misleads the reader. |
|
|
223
|
+
| <a id="one-subject-per-spec"></a>`one-subject-per-spec` | model | checker | spec | Every chain of a spec file reaches ONE subject, read statically: an api chain its method and route template (`GET /events/:id`, the request line of a `.http` a `.request(file())` sends), a job chain the job it triggers, a cli chain its command words, a website chain the page it visits, a mobile chain the journey it opens. A mobile test that opens the app a second time (the relaunch that proves what its first open left behind) reaches the journey of its first open alone. A file reaching several is an error at the first chain that reaches a second. The finding prints the plan: the tests each subject takes, and the folder it files under, the subject's standard folder under the specification as `spec-in-subject-folder` names it. The split is its author's, who names each file: `typescript fix` writes none, since naming a file is not a rewrite a tool can take. A test whose own chains reach two subjects is reported and left. A spec of a SPECIMEN is out of reach, one whose runner drives an app under a `__fixtures__/` (its specification's or the pool's) that its specification module, or the one that module imports a runner from, names in a literal: its routes, jobs and commands are no subject of the project. | Split it by hand along the plan the finding prints, one file per subject, each named by you. A test reaching two subjects states what it stands on with a seed instead of a second action. | A file about one subject is found by its subject: the reader of `GET /events/:id` opens one folder and reads every question asked of it, and a change to that route touches one file. |
|
|
224
|
+
| <a id="reserved-folder-in-closed-list"></a>`reserved-folder-in-closed-list` | model | checker | specs | A folder under a specs tree whose name starts with `_` is never a subject, and the special folders form a closed list: `__setup__/` (the TypeScript that prepares specs), `__seeds__/`, `__requests__/`, `__expected__/`, `__fixtures__/` (the ground); at the specs root, they hold what the specifications share, as a folder's hold what its leaves share. Any other `_` name is `special-folder-in-closed-list`'s. A folder with a special name but no marks (`setup/`, `seeds/`, `requests/`, `expected/`, `fixtures/`, and `doubles/` for test adapters) is an error wherever it holds no spec, the fix naming its special folder (`__setup__/` for `doubles/`), and so is a `shared/`, whose fix is the special folders of the folder that encloses it; one holding a spec is a subject (`GET /requests` files under `api/requests/list/`) and passes, except directly under the specs root, which holds no subject. Inside a `__setup__/` the names are the code's own and no bare reserved name is judged, but `__setup__/` holds code alone: a ground folder in it is an error, and so is any file that is no module (`.ts`, `.tsx`, `.js`, `.mjs`, a `.d.ts` and their kin), the fix naming the ground folder of its role beside that `__setup__/`: `__seeds__/` for a `.sql`, `__requests__/` for a `.http`, `__expected__/` for what a test compares against, `__fixtures__/` for any other input. Ground is never entered: what a fixture holds is its own. | Rename the folder to the special folder that says what it holds, and the paths that name it; move what a `shared/` holds into the special folders of the folder that encloses it. | A route, a command or a job names its subject folder, so a subject can take any word: only a marker keeps the folders that are no subject from colliding with one, and only a closed list keeps the marker meaning something. |
|
|
225
|
+
| <a id="setup-module-at-lowest-common-folder"></a>`setup-module-at-lowest-common-folder` | model | checker | specs | A `__setup__/` module sits in the `__setup__/` of the LOWEST folder every module importing it shares, the law ground follows (`ground-shared-or-inline`) read on imports. A reader is any module of the tree importing it by a relative path or a `package.json` `imports` alias, and a reader inside a `__setup__/` counts as the folder that `__setup__/` belongs to. Readers meeting only at the specs root share `<specs>/__setup__/`. A module one spec or test file alone imports is an error at the module: it belongs in that file, written in place, unless the two together would pass the 400 lines `spec-file-within-line-limit` holds a spec to (it then stays a file in its reader's `__setup__/`); a module under `stubs/` is excepted, since a stub is imported through its facade. A module no module of the tree imports is out of this check's reach and knip's to report, as an unused file: a config may name it (a vitest `setupFiles` entry), which no import shows. Fixed by `typescript fix`: a module off its readers' lowest folder moves to that folder's `__setup__/` (a `git mv` in a work tree), with its own relative literals, the `'..'` pieces it climbs from its own folder by (`resolve(import.meta.dirname, '..', '..')`), and every importer's path to it rewritten; one a reader reaches through an `imports` alias is left where it is, since the alias is the package's to rewrite; one a single spec reads is written into that spec by its author. | Run `typescript fix`, which moves it to the `__setup__/` the finding names and rewrites the paths that import it, or write it in its one reader. | Where shared code sits says who shares it: a helper parked above its readers reads as everyone's and one parked beside one of them reads as that leaf's, so the reader cannot trust the folder to tell them what a change reaches. |
|
|
226
|
+
| <a id="spec-code-in-setup-folder"></a>`spec-code-in-setup-folder` | model | checker | specs | Under a specs tree, a module (`.ts`, `.tsx`, `.js`, `.mjs` and their kin) that is no spec (`*.spec.ts`), no test (`*.test.ts`, `*.test.tsx`) and no runner (`*.specification.ts`) sits under a `__setup__/`: a test adapter, a shared value, a helper, a stub facade and its kind folders (`__setup__/stubs/`). Ground is out of reach, and so is a rendered unit (the `.tsx`, or the `.ts` of a hook) beside the `*.test.tsx` of the same name that renders it (`test-in-its-tests-folder`): it is that test's subject, not code that prepares it. A `*.fixtures.ts` is an error anywhere under the tree, `__setup__/` included: a fixture is input data (a file in `__fixtures__/`), so a module that prepares specs is named for what it holds. A file directly at the specs root is `specs-root-holds-specifications-and-shared`'s. | Move the module into the `__setup__/` of the lowest folder its readers share, named for what it holds, and the paths that import it. | A spec folder holds specs, and a reader listing it expects questions asked of the subject: code beside them is a second kind of file mixed in, and a module called `fixtures` promises data it does not hold. |
|
|
227
|
+
| <a id="spec-in-subject-folder"></a>`spec-in-subject-folder` | model | checker | spec | A spec file about one subject sits in the subject's standard folder, derived and never listed: `GET /events` files under `events/list/`, `GET /events/:id` under `events/get/`, `POST /events` under `events/create/`, `PUT\|PATCH /events/:id` under `events/update/`, `DELETE /events/:id` under `events/delete/`, a route under an item (`GET /events/:id/timeline`) its literal segments (`events/timeline/`), a singleton (`GET /health`) `health/get/`, a method-less route, one `jterrazz.config.ts` names so (`test: { methodless: { '/mcp': 'the MCP transport answers GET and POST' } }`), reached by whatever verb a test sends, and never split by it (`one-subject-per-spec`), its segments (`mcp/`); a job its name (`specs/job/<job>/`), a cli command its first word (`specs/cli/<word>/`), or every leading word the `commands` list of the runner its chain's handle came from holds (`specification.cli(bin, { commands: ['db', 'migrate'] })` files `db migrate` under `db/migrate/`; the list written in the call or bound to a `const` of the runner's file). A route of an app mounted under a base path `jterrazz.config.ts` declares (`test: { basePath: '/api' }`) is filed by its path past that base: `GET /api/posts` files under `posts/list/`, never `api/posts/list/`. A GET on one segment lists a plural noun, or a collection `jterrazz.config.ts` names (`test: { collections: ['data'] }`), and gets anything else; a segment is a parameter when the chain writes the route's template (a `:name` segment, or `{ params }` beside the path, `route-written-as-template`), or when it is a template hole or anything but lowercase words. A filled path whose word sits right after a collection with more path after it (`btc` in `/indicators/btc/chart`) reads two ways: it is reported at its chain, naming the template to write (or `{ params: {} }` when the word is the route's own) and never moved; a `.http` request line is read as the route. A page, a journey and the root route have no standard name, and are not judged, and neither is a spec of a specimen (`one-subject-per-spec`). An error at the file's first line; fixed by `typescript fix`, which moves the file whole, rebasing the paths of its `file()`, `directory()`, imports, `vi.mock()` and `new URL(..., import.meta.url)`, and leaving the file to its author when a relative path no rebase carries would break, and removes the folders the move leaves empty. | Run `typescript fix`: it moves the file to its subject's folder, and the ground it stands on with it. | A name derived from the subject is the same in every repository: nobody lists the folders, nobody argues them, and a reader who knows the route knows the path. |
|
|
228
|
+
| <a id="specification-folder-has-its-runner"></a>`specification-folder-has-its-runner` | model | checker | specs | A first-level folder named for one of the seven specifications holds a `*.specification.ts(x)` calling `specification.<specification>(`. Positive-only: any other first-level name is a repository suite, judged by `specs-at-declared-depth`'s declared depth alone. | Create the specification the folder promises, or rename the folder. | Folder = constructor is the law the fork rests on; a specification folder with no runner tells the reader something false before they open a file. |
|
|
229
|
+
| <a id="specification-test-uses-its-runner"></a>`specification-test-uses-its-runner` | model | checker | specs | A test under `specs/<specification>/`, where the specification root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself. Ground is out of reach: nothing under a `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` folder is a spec, a `.test.ts` under `__expected__/` being the GOLDEN a fixer writes. So is any first-level folder that is not a specification: a specification with no module to sit beside and no runner to reach lives in a repository suite there. | Import `{ <specification> }` from `<specification>.specification.js`, move a module test into the `__tests__/` of its module's folder, or move a repository suite to a first-level folder of its own under `specs/`. A module of the specs tree itself has no module test (its `__tests__/` would be a special folder the ground refuses), so it moves out of `specs/` into a code root (`src/`, or one `typescript.roots` declares) with its test. | A test reaching no runner under a specification is a module test paying the specification's budget and services to prove something that belongs in its module's `__tests__/`. |
|
|
230
|
+
| <a id="golden-path-creates-folders"></a>`golden-path-creates-folders` | model | runtime | ground | A golden's path may name a subfolder (`./__expected__/build/verbose.txt`): the path is the file, and update mode creates the folders it names. | Name the subfolder in the path. | A flat folder stops being readable at a dozen files, and a path that names its subfolder is read where it is written: no second option says where a golden lives. |
|
|
231
|
+
| <a id="folder-follows-assets"></a>`folder-follows-assets` | model | process | specs | The folder follows the assets: a test with asset directories of its own gets its own domain; tests with no local assets group as sibling `<aspect>` files. The static rule only checks the depth. | Read what the assets say: a test that stands on its own ground earns a folder. | It is the assets that settle the grouping: a criterion no channel can decide alone. |
|
|
232
|
+
|
|
233
|
+
## expectations: What is compared, and how
|
|
234
|
+
|
|
235
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
236
|
+
| ------------------------------------------------------------------------------------- | ------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
237
|
+
| <a id="expected-constant-shared"></a>`expected-constant-shared` | model | static | spec | A file-level `const` a spec hands to a matcher as the expected value serves two tests or more; read by one test, it is an error at its declaration. A constant read outside every test (by a helper) counts as shared, and an exported one is out of reach. | Write the value in place, in the matcher the one test hands it to, and delete the constant: `typescript fix` inlines one read. | A test is read where it is written: an expectation parked at the top of the file makes its one reader scroll to know what it asserts, and buys no sharing for the price. |
|
|
238
|
+
| <a id="negative-golden-frozen"></a>`negative-golden-frozen` | model | static | tests | A golden `toMatch(file(...))`/`toMatch(directory(...))`, or a `toMatch(<value>)` on a result (the value written in place, rewritten in the test), whose FAILURE is the subject of the test must carry `{ frozen: true }`, or `TEST_UPDATE=1` silently rewrites the deliberately-wrong expectation instead of throwing; its absence is an error. Two shapes are read: the exact wrapper (`expect(() => ...).toThrow()` / `expect(...).rejects.toThrow()`) and a bounded heuristic, a golden inside a helper whose body also asserts a throw, which is where a helper owning the try/catch puts it. | Pass `{ frozen: true }` so update mode never overwrites the fixture the test is about. | In update mode an unfrozen matcher writes instead of throwing: the negative fixture is corrupted by its own real output and the assertion stops testing anything. `frozen` pins it. |
|
|
239
|
+
| <a id="no-double-only-assertion"></a>`no-double-only-assertion` | model | static | module | A module test whose every assertion reads the call log of a double it built itself (`mockOf`, `vi.fn`, `vi.spyOn`) is an error. The reach is the `module` role: on a component a callback prop IS the contract with its parent. It is an error, and not an error, because one subject answers this way honestly (one whose PRODUCT is a call it was handed: a listener, an output port), and the shape is the same one either way. | Assert the returned value or the resulting state, or say in a line that the call IS the product. | The subject can return anything, raise anything, or return nothing at all, and every assertion still passes. |
|
|
240
|
+
| <a id="no-empty-substring-match"></a>`no-empty-substring-match` | model | static | tests | `toMatch('')`, or `toMatch` of an empty template literal, on a plain string (a stream's `.text`, `.raw` or a file's `.content`) is vitest's substring match, which holds for any text, and is an error. The empty value is the "not recorded yet" mark of an accessor handed to `toMatch`, which update mode fills; on a string it only passes. | Compare the string whole with `toBe('')`, or hand the accessor itself to `toMatch`. | `'abc'.includes('')` is true: the assertion cannot fail, and a stub the author meant to fill later stays green. |
|
|
241
|
+
| <a id="no-existence-only-assertion"></a>`no-existence-only-assertion` | model | static | tests | A test whose ONE assertion is an existence check (`toBeDefined`, `not.toBeNull`, `not.toBeUndefined` or a bare `toHaveBeenCalled`) is an error. Beside a real assertion the same check is a precondition and stays silent, `expect.soft` counts as an assertion, and a NEGATED bare matcher (`not.toHaveBeenCalled`) is a precise proof rather than a loose one. `toBeTruthy` and a bare `toThrow` are refused wherever they stand, by `vitest/prefer-strict-boolean-matchers` and `vitest/require-to-throw-message`. | Assert the value, a golden, or a row. | `toBeDefined()` passes for `0`, `[]`, the wrong object and the right one: as a whole proof it records that the call returned, not what it answered. |
|
|
242
|
+
| <a id="no-expect-in-a-loop"></a>`no-expect-in-a-loop` | model | static | tests | An `expect()` in a test is not called inside a loop (a `for`, a `for...of`, a `for...in`, a `while`, a `do...while`) nor inside the callback of an array iteration (`forEach`, `map`, `flatMap`, `filter`, `every`, `some`, `find`, `reduce`) in the same function: a loop runs its assertion once per element, and not at all over an empty collection. `vitest/no-conditional-expect` holds the other assertion that may not run, one under a condition. | Compare the whole collection in one assertion, `expect(rows.map(...)).toStrictEqual([...])`, or state each case with `test.each`. | A loop over nothing passes with no assertion made, and a loop that fails names one element where a whole comparison would show them all. |
|
|
243
|
+
| <a id="no-expect-outside-a-test"></a>`no-expect-outside-a-test` | model | static | specs | Under `specs/`, vitest's `expect` is called in a test file alone (a `*.spec.ts`, a `*.test.ts(x)`, a `*.test-d.ts`): a `__setup__/` module, a test adapter, a stub or a `*.specification.ts` that asserts is refused at the call. The binding is what is read (`expect` imported from `vitest` under any local name, statically or by `await import('vitest')`, `<namespace>.expect` on a namespace of `vitest`, and `expect.soft`, `expect.poll` on either), so a method or a local of that name is not. | Return what the module reads, and assert it in the test that calls it. | An assertion in a helper fails far from the test that owns the claim, fires alike for every caller, and takes the Then out of the test body where a reader looks for it. |
|
|
244
|
+
| <a id="no-imported-expectation"></a>`no-imported-expectation` | model | static | spec | A spec imports no expectation: a name imported from another module of its specs tree (neither its `*.specification` runner nor a module under `stubs/`) and handed to a matcher as the expected value, itself or a member of it, is an error at the argument. A helper is CALLED, and what it builds is the test's own; a module outside the specs tree is the product's, and a spec may compare against its data (`package.json`'s version, a constant the product exports); `no-test-to-test-import` already refuses importing a test file at all. | Write the expected value in the test (in place, or `file(...)` when several tests of the leaf share it) and drop the import. | An expectation written in another module asserts what nobody reviewing the test reads, and a change to that module moves the assertions of every spec that imports it without one of them changing. |
|
|
245
|
+
| <a id="no-probe-cluster"></a>`no-probe-cluster` | model | static | tests | Three probes (`threshold`, default 3) on one goldenable subject (`stdout`, `stderr`, `content`, `head`, `meta()`, `canonical`, `alternates`, `tree`, `html`) with no `toMatch(file(...))` on it is an error. A negated probe states an absence no golden can carry and is not counted; `value` and `error` are single readings and are not goldenable subjects. | Golden the subject: `expect(result.stdout).toMatch(file('./__expected__/<case>.txt'))`, tokens for what moves. | Three greps state three lines, leave everything between them unstated, and still have to be rewritten one by one the day the output changes. |
|
|
246
|
+
| <a id="no-raw-bypass"></a>`no-raw-bypass` | model | static | tests | `expect(x.raw).toContain` compares the stream as it was captured, and is an error. A stream compared as a value reads `x.text`: ANSI stripped, the runner's `transform` applied, the text a golden compares. A RegExp argument is exempt: a pattern carries no token grammar to lose. | Compare `x.text`; keep `.raw` for a test that proves what the comparison strips, with the suppression saying so. | `.raw` keeps the colour codes and skips the runner's transform, so a substring found there is not the text any golden of the same stream would be compared against. |
|
|
247
|
+
| <a id="no-sampled-value-in-expectation"></a>`no-sampled-value-in-expectation` | model | static | tests | No sampled value under an oracle: `new Date()` (no argument), `Date.now()`, `performance.now()`, `Math.random()` and `randomUUID()` are an error as a direct argument of `expect` or of its matcher, and anywhere inside a structural matcher's expected shape. A test that pins the clock (`clock.at`, `clock.advance`) is out of reach (the reading is then the constant it chose) and so is a `*.specification.ts(x)`, where a runner legitimately samples at startup. | Pin it with `clock.at()`, or match it with `{{iso8601}}`/`match.uuid`. | An assertion that reads the machine twice holds whatever the subject does, and the day it fails it fails for the clock. |
|
|
248
|
+
| <a id="no-single-use-ref"></a>`no-single-use-ref` | model | static | tests | A capture ref (`match.ref`, `{{kind#ref}}`) appearing exactly once in the whole file (its code, a `match.ref` and every value written in place, a string or a template, plus the golden files it references) is named for nothing, and is an error. | Drop the ref name and keep the bare token. | A ref only earns its name when it asserts equality across at least two occurrences. |
|
|
249
|
+
| <a id="no-status-only-test"></a>`no-status-only-test` | api | static | tests | A test whose ONLY assertions are HTTP status probes (`expect(X.status).toBe(N)` / `.toEqual(N)`, N a numeric literal from 100 to 599) is an error: this case wants a full golden (`expect(result.response).toMatch(file('./__expected__/case.http'))`). A status probe BESIDE a real assertion (a whole-result `toMatch`, a table's rows, `toContain`...) stays silent (a legitimate scalpel). | Assert what the response carries, not only the code it came back with. | A lone status pins the response code and throws away the whole payload; a full golden captures the shape and its token grammar (it completes `result-compared-whole`, which reads the response the probe stands beside). |
|
|
250
|
+
| <a id="no-transform-duplicating-tokens"></a>`no-transform-duplicating-tokens` | model | static | tests | A `transform` that only rewrites into equivalents of the standard tokens duplicates the grammar, and is an error. | Delete the transform and write the token the grammar already has. | `transform` is the escape hatch for noise the tokens do not cover, not a second copy of them. |
|
|
251
|
+
| <a id="result-compared-whole"></a>`result-compared-whole` | model | static | tests | A test that reads a response, a json or a table (`<result>.response`, `<result>.json`, `<result>.table('<name>')` of a result it awaited) compares it WHOLE, once: `toMatch` handed the whole result written in place or its golden, or a table's `toBeEmpty()`. A precise assertion on one of its properties (`expect(result.json.value.total).toBe(1)`) is allowed only IN ADDITION, and the first read of a result no whole comparison reaches is an error. A negation, a RegExp and a read through a helper are no whole comparison; a result read through a helper, a computed table name, or a comparison made in a helper is out of reach, and the rule stays silent. A read whose value is only CARRIED into a later request of the same test (every use of it, through the names and helpers it passes, ends in the arguments of a chain off a handle the test awaits, in place or through the name that holds it: a path, `params`, a body, a header, as the cursor a page hands back) claims nothing and owes no comparison; one `expect(...)` of that value makes it a read again, and the result the test does claim is still compared whole. | Write `expect(<result>.<subject>).toMatch('')` and run `TEST_UPDATE=1`: the whole result is written in place; keep the precise assertion beside it if one property IS the subject. | Probes are a partial oracle: what they do not name is free to regress unseen. The whole result, written where the test reads it, is the default oracle (approval testing) and a precise assertion states what the test is about on top of it. |
|
|
252
|
+
| <a id="no-vitest-snapshot"></a>`no-vitest-snapshot` | model | upstream | tests | No vitest snapshot matcher: `toMatchSnapshot`, `toMatchInlineSnapshot`, `toMatchFileSnapshot`, `toMatchAriaSnapshot`, `toMatchScreenshot`, `toThrowErrorMatchingSnapshot` and `toThrowErrorMatchingInlineSnapshot` are refused by `vitest/no-restricted-matchers`, whose option the `test()` preset sets (ADR-005). | Write the golden: `expect(subject).toMatch(file('./__expected__/<name>.<ext>'))`; under `src/` compare to a literal with `toStrictEqual`. | A snapshot is written by the run that was supposed to be judged by it: nobody reads the diff, and a wrong answer becomes the expectation the moment someone re-records. |
|
|
253
|
+
| <a id="document-follows-its-grammar"></a>`document-follows-its-grammar` | cli | checker | document | A `<case>.spec.yaml` document follows its grammar: closed keys (`description`, `fixture`, `env`, `serve`, `runs`) and closed keys inside a run; `description:` and `runs:` required; `command:` and `exit:` required in every run, `exit:` an integer literal, `waitFor:` only on the last run and never with `stdin:`, a `files:` path relative and under the workdir; every `fixture:` path relative, reaching `__fixtures__/`. | Write the key the grammar names; the message prints the accepted set. | The grammar is read by the runner's own parser: the document lint accepts is exactly the one the runner runs. |
|
|
254
|
+
| <a id="http-first-line-follows-its-grammar"></a>`http-first-line-follows-its-grammar` | api | checker | ground | The first line of a depth-1 `.http` follows its grammar: a request line (`METHOD /path`) under `__requests__/`, a status line (`HTTP/1.1 <status>`) under `__expected__/`. | Write the opening line the folder calls for, or move the file to the other one. | The opening line is what tells a request from a response: constraining it catches misplaced files. |
|
|
255
|
+
| <a id="no-empty-expected-stream"></a>`no-empty-expected-stream` | cli | checker | document | An expected stream whose whole content is `{{any}}` is an error: the run executes and proves nothing. | Regenerate with `TEST_UPDATE=1` and keep tokens only for the parts that move. | The `vitest/expect-expect` mirror for documents: an assertion that accepts anything is an absent assertion. |
|
|
256
|
+
| <a id="no-malformed-ref"></a>`no-malformed-ref` | model | checker | ground | A malformed ref of a known kind (`{{iso8601#}}`, `{{uuid #id}}`) in a text file under `__expected__/` is an error. | Write `{{kind#name}}` with no space, and a name for the ref. | A malformed capture would fail silently: naming it makes it visible early. |
|
|
257
|
+
| <a id="no-silent-refusal"></a>`no-silent-refusal` | cli | checker | document | A run whose `exit:` is non-zero with neither `stdout:` nor `stderr:` is an error: the document does not say why the command refused. | Write the refusal the command prints: that is what the case proves. | A silent refusal is either a defect in the product, or a golden that forgot the words went to stderr. |
|
|
258
|
+
| <a id="no-token-in-request"></a>`no-token-in-request` | api | checker | ground | A known token in a file under `__requests__/` is an error: requests are inputs, never matched. | Write the literal value the request actually sends. | A token in an input is neither validated nor substituted: it is almost always a mistake. |
|
|
259
|
+
| <a id="no-unknown-token"></a>`no-unknown-token` | model | checker | ground | Every `{{token}}` in an `__expected__/` fixture (or in an expected stream of a `<case>.spec.yaml` document) belongs to the frozen vocabulary; an unknown token is an error. | Write a token the grammar knows; the message prints the full list. | A closed grammar shared with the runtime matcher stops the two channels drifting. |
|
|
260
|
+
| <a id="no-volatile-literal"></a>`no-volatile-literal` | cli | checker | ground | No expected output carries a literal volatile value: a loopback origin with a port (`127.0.0.1:8080`, `localhost:3000`), an absolute temporary path (`/tmp/`, `/private/tmp/`, `/var/folders/`) or a home path (`/Users/...`, `/home/...`). Both halves of the ground are judged by the one list: an expected stream of a `<case>.spec.yaml`, and every text file under `__expected__/`. A `{{token}}` is not a literal, and neither is a value the DOCUMENT pinned: in a stream, a word the same document states in a command argument, an `env:`/`serve:` value, a `stdin:`, a `fixture:` it names or its own sibling `__fixtures__/` is excluded before it is judged. | Write the token the message names (`{{workdir}}`, `{{url}}`) in place of the literal. | This is exactly the shape a `TEST_UPDATE` run would have tokenised: finding one means a token was overwritten by hand. |
|
|
261
|
+
| <a id="pinned-value-from-the-input"></a>`pinned-value-from-the-input` | cli | checker | document | A literal ISO-8601 timestamp or uuid in an expected stream of a `<case>.spec.yaml` is an error, unless the same value appears in a `fixture:`, the document's sibling `__fixtures__/`, a `stdin:`, a command argument or an `env:`/`serve:` value of the same document (it is then pinned, not volatile). Under `__expected__/` the same class waits for `no-unseeded-value-in-golden`, whose criterion the Conventions page carries. A value a document cannot show where it is pinned (a `serve:` stub's clock, the product's own wording) is declared above its stream, `# checker-disable-next-line pinned-value-from-the-input -- <reason>`: no comment can stand inside a block scalar, so the directive on the stream's key silences every line of it. | Token it, or seed it: a value the document pins is a value it may assert. | The defect is not the literal: it is the literal nothing pinned. |
|
|
262
|
+
| <a id="expected-headers-are-a-subset"></a>`expected-headers-are-a-subset` | api | runtime | ground | An `__expected__/*.http` starts with a status line, and its headers are matched as a SUBSET: the ones listed must match, unlisted response headers are unconstrained. | List the headers the case is about; leave the rest out. | A response carries headers no test is about (dates, lengths, proxies); matching them all would make every golden a record of the machine that wrote it. |
|
|
263
|
+
| <a id="golden-read-from-its-path"></a>`golden-read-from-its-path` | model | runtime | tests | Every golden reads the `file()` or `directory()` it is given, from the spec file's directory (response, stream, json and tree alike), and `request()` reads a request the same way. Nothing resolves against a folder the line does not name. | Name the golden from the spec: `file('./__expected__/<case>.<ext>')`. | One resolution rule, written on the line, means a reader knows where a golden lives without knowing any convention or which accessor produced it. |
|
|
264
|
+
| <a id="one-verb-per-meaning"></a>`one-verb-per-meaning` | model | runtime | tests | `toMatch` on a result the framework produced compares the WHOLE result, to a value written in place or to a `file()`/`directory()` golden. Its argument says where the expectation lives: a value written in place (a stream's text, a response's `.http` text, a json's value, a table's rows, awaited) or a `file()`/`directory()` golden. On a plain string it is vitest's own, a string or a RegExp, and a `file()` or a `directory()` handed to it is refused, a runner started or none (the path object would otherwise read as the pattern `[object Object]`); a string is goldened wrapped, `expect(text(value)).toMatch(file(...))`, and a stream's text is `result.stdout.text`. A value of the wrong shape (a RegExp on a stream, a response or a json, anything but rows on a table) does not compile, and past the compiler it is refused in both builds, naming the form that takes it; a directory, the working directory and a container take no value, and are refused naming `directory()` or `toBeRunning()`; a golden handed a subject that has none of its kind is refused, naming the wrapper the subject takes. An empty stream is an empty text: `toBeEmpty` is a table's matcher, and handed a stream it is refused in both builds, naming `expect(<stream>.text).toBe('')`. | Write the whole result in place, ``toMatch(`...`)``, or `toMatch(file('./__expected__/<case>.<ext>'))` when it lives on disk; a part of a text is vitest's, on `.text`. | A result is not a string, so vitest's substring meaning never applied to it: one verb compares it whole, the value in place when the test is its only reader, a branded `file()` when the expectation lives on disk, and the runtime refuses the crossed form a type cannot see (a cast, a JavaScript spec). |
|
|
265
|
+
| <a id="result-accessor-read-only"></a>`result-accessor-read-only` | model | type | tests | A result accessor is read-only. | Assert on what the run produced; change the Given if the answer is wrong. | The answer a run produced is evidence, and a test that can rewrite its own evidence proves nothing. |
|
|
266
|
+
| <a id="output-asserted-as-a-golden"></a>`output-asserted-as-a-golden` | model | process | tests | A tool's output is asserted as a full golden per scoped use case, not as a cluster of greps; `.grep()` stays the scalpel for targeted probes. | Golden the stream, and keep `.grep()` for the one line a case is genuinely about. | A review judgement: the static channel cannot tell a legitimate grep from a lazy one. |
|
|
267
|
+
|
|
268
|
+
## stubs: What the outside world replies, and where the stubs are filed
|
|
269
|
+
|
|
270
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
271
|
+
| ----------------------------------------------------------------------------------- | ------------- | ------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
272
|
+
| <a id="stub-data-has-a-builder"></a>`stub-data-has-a-builder` | model | static | stub | Inside a kind folder of `__setup__/stubs/`, every `*.request.ts` (the exact request a unit matches) has a `<stem>.ts` sibling, the stem the name up to the FIRST dot (`classify-article.ts` for `classify-article.request.ts`). Orphan data is an error, reported on the orphan itself. A payload a unit serves is data, so it sits in the ground beside the `__setup__/` (`__fixtures__/<name>.json`), which `__setup__/` never holds (`reserved-folder-in-closed-list`). | Write the stub that matches the request, or delete the request. | Data exists only as served by a stub: a file with no owner is dead weight no test ever loads. |
|
|
273
|
+
| <a id="stubs-imported-through-facade"></a>`stubs-imported-through-facade` | model | static | tests | Outside `specs/**/__setup__/stubs/`, an import resolving into a kind folder (`__setup__/stubs/<kind>/...`, `<kind>` a stub builder the package exports: `http`, `openai`, `anthropic`) is an error: a test imports only the `*.stubs.ts` facades. | Add a named scenario export to the facade and import that instead. | Unit stubs are composition details; going through the facade keeps each scenario named in the same place as the world it derives from. |
|
|
274
|
+
| <a id="stubs-root-holds-facades-and-kinds"></a>`stubs-root-holds-facades-and-kinds` | model | static | stub | Under `specs/**/__setup__/stubs/`: the ROOT carries only `*.stubs.ts` facades and one folder per stub KIND, a kind being a stub builder `@jterrazz/test/stubs` exports (`http` \| `openai` \| `anthropic`, the closed list its exports decide). A facade exports its builders and sets BY NAME (the world and its scenarios), and one that exports nothing is an error; a default export is not required, and when a facade has one it is a composition: built by `defineStubs(...)`, a composition re-export, a scenario of a sibling facade (a name imported from a `*.stubs` module, or a call to it) or a call to a builder of the file declared to return `Stubs` (no type is read, so a builder is judged by where it comes from or what it declares). A unit stub is `<kind>/<kebab-name>.ts` with an `export default defineStub(...)` (or a factory returning one), and it calls only its own folder's builder: a `<kind>.<builder>` of another kind the file imports from `@jterrazz/test/stubs` (by name, or through a namespace import, `t.<kind>.<builder>`), in its request, its reply or anywhere else, is an error. A kind folder is flat (a file nested below it is an error), and a folder under the root that is no kind is an error at each unit it holds; each file is judged on its own, and a file that is no module is `reserved-folder-in-closed-list`'s. | Move the file to the half it belongs to: a facade at the root, a unit under the folder of the builder it calls. | A fixed tree separates the public facade from the internal units and makes the FOLDER carry the kind: filenames go back to being domain words, and a unit reads as one kind of outside world. A test imports the builder it calls by name, so a required default no test reads would be dead code the unused-export pass reports. |
|
|
275
|
+
| <a id="network-strict-after-first-stub"></a>`network-strict-after-first-stub` | model | runtime | tests | From its first stub, a chain's network is strict: every outgoing request matches a declared stub. As soon as an `api`/`job` chain declares an intercept, any unmatched outgoing request (an exhausted queue included) fails the spec with an explicit error. | Declare the stub the request needs, or stop making the request. | A guarded network makes external interactions exhaustive and intentional. |
|
|
276
|
+
|
|
277
|
+
## services: The services a run starts, and the seeds laid in them
|
|
278
|
+
|
|
279
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
280
|
+
| ----------------------------------------------------------------- | ------------- | ------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| <a id="seeds-are-sql"></a>`seeds-are-sql` | model | static | ground | `__seeds__/` holds `*.sql` files only; any other file is an error. | Write the seed as SQL, or move the file to `__fixtures__/` as the material it is. | `.seed()` carries database state only: no seed handlers, no prefix dispatch. |
|
|
282
|
+
| <a id="seeds-state-absolute-time"></a>`seeds-state-absolute-time` | model | checker | ground | A `__seeds__/**/*.sql` states absolute timestamps: `CURRENT_TIMESTAMP`/`CURRENT_DATE`/`CURRENT_TIME`, `NOW()` and the quoted `'now'` SQLite's date functions take (`datetime('now')`, `strftime(..., 'now')`, `date('now', '-1 day')`) are refused. A comment is not state, so a clock read inside one is not a finding. | Write the instant the case is about, and pin the run's clock with `.clock()` on the chain. | A seed that reads the clock makes the row's date the date the suite happened to run: a case about "published yesterday" proves something different every day, and under SQLite `datetime('now')` writes a space-separated text where an ORM writes ISO `T`, so the two misorder across a calendar day. The estate dry run (the whole catalogue over every repository of the workbench) counts 36, every one of them signews-api's. |
|
|
283
|
+
|
|
284
|
+
## clock: Time: the pinned instant, sleeps and sampled values
|
|
285
|
+
|
|
286
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
287
|
+
| ------------------------------------------------------------------------- | ------------- | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
288
|
+
| <a id="no-sampled-value-in-test"></a>`no-sampled-value-in-test` | model | static | tests | A sampled value anywhere in a test callback is an error. It is the wider net around `no-sampled-value-in-expectation`, silent where that rule already refuses, on a name built in a template literal, and on a `new Date()` or `Date.now()` after a `using _ = clock.at(<instant>)` or `clock.run(<instant>)` in the same block, where the reading is the pinned instant. A pin fakes `Date` alone, so `Math.random()`, `randomUUID()` and `performance.now()` stay samples under it, and an argument-less `clock.run()` pins no instant. A `*.specification.ts(x)` is out of reach. | Reach for `clock.at()`, `clock.advance()`, or a token in the golden. | A value sampled into the Given travels into the subject, and the assertion that reads it back is the same tautology one step removed. |
|
|
289
|
+
| <a id="no-sleep"></a>`no-sleep` | model | static | tests | No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file: synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation is out of reach: the double is staging the world, not the test waiting on it. A double is a factory call (`vi.fn(...)`, `mockImplementation(...)`, a `mockOf` handler) or a function-valued PROPERTY of an object literal, which is the plainest double there is: `const git: GitGateway = { cloneRepository: async () => ... }`, where the object itself is the double. | Wait for the condition, not for a duration. | A fixed sleep makes tests slow and flaky; waiting on a condition is deterministic. |
|
|
290
|
+
| <a id="no-vitest-time-or-global-stub"></a>`no-vitest-time-or-global-stub` | model | upstream | tests | `vi.stubGlobal`, `vi.useFakeTimers`, `vi.setSystemTime` and `vi.useRealTimers` are refused by `vitest/no-restricted-vi-methods`, each with the primitive that replaces it: a subject that fetches a relative URL states its base with `intercept(stubs, { origin })`. `vi.stubEnv` is sanctioned; `vi.mock`/`vi.doMock` stay on `no-module-mock`, whose allow-list the option cannot express. | Reach for `intercept()` and `clock`: both give back what they took at the end of the scope. | A stubbed global is the network replaced by a function the test wrote, and a fake timer taken without `using` outlasts the test that took it. |
|
|
291
|
+
|
|
292
|
+
## mocks: Doubles of a port
|
|
293
|
+
|
|
294
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
295
|
+
| ------------------------------------------------------- | ------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
296
|
+
| <a id="no-module-mock"></a>`no-module-mock` | model | static | tests | A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (each entry `{ module, reason }`, and an entry with no reason clears nothing), a `__mocks__/` directory, and, in a `module`-role test, importing a data asset (`.json`, `.sql`, `.yaml`, ...) are all errors, a JSON file imported `with { type: 'json' }` excepted: it is data the product reads as it does, not a mock. The specifier is read in BOTH spellings, `vi.mock('<spec>')` and the `vi.mock(import('<spec>'))` form `vitest/prefer-import-in-mock` requires, so an allow-listed module has a green spelling under both rules. A dotted specifier (`./dashboard.post`) is still code. | Double the port with `mockOf<Port>()`, keep a module test's payload in a `__tests__/<file>.fixtures.ts` beside it and a spec's in `__fixtures__/`, and allow-list a native module as `{ module, reason }` (Module tests § Doubles). | A module test that reaches for a real file or a module mock is describing an assembled product, and that has a specification of its own. |
|
|
297
|
+
| <a id="mock-of-needs-a-port"></a>`mock-of-needs-a-port` | model | type | tests | `mockOf<Port>()` needs its port: `T` is constrained to `object` with no default, so a double built without one has no surface and every call on it is refused. | State the port: `mockOf<PaymentGateway>()`. | The type argument IS the contract the double stands for; without it the double agrees with anything. |
|
|
298
|
+
|
|
299
|
+
## visitor: Screens, targets and scenarios
|
|
300
|
+
|
|
301
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
302
|
+
| --------------------------------------------------------------------------------- | ------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
303
|
+
| <a id="no-expect-in-scenario"></a>`no-expect-in-scenario` | model | static | tests | A scenario (`.visit()` on website, `.open()` on mobile, `.render()` on component) is the When: the visitor acts and the capture reflects the settled state. No `expect()` stands in the callback; assertions live in the Then, on the returned result. | Move the assertion out of the callback, onto the result the terminal action resolves to. | Separating interaction from assertion keeps the grammar of setup, action and result intact and the scenarios replayable. |
|
|
304
|
+
| <a id="scenario-ends-settled"></a>`scenario-ends-settled` | model | static | specs | A scenario under `specs/` that ACTS (`click`, `fill`, `select`, `check`, `press`, `tap`, `rerender`) on the visitor it was handed ends on `visitor.see(...)`, `visitor.gone(...)` or `visitor.unmount()`, through the block, branch, loop or `.then()` it ends in. A scenario that only reads is out of reach, and so is a component test, whose action often produces a CALL rather than a screen. | End on `visitor.see(<what the action produced>)` or `visitor.gone(<what it removed>)`. | The capture is taken when the callback returns: a scenario ending on a click hands the golden whatever was on the screen at that instant, and the failure reads as flakiness rather than as a missing wait. |
|
|
305
|
+
| <a id="test-id-states-what-is-missing"></a>`test-id-states-what-is-missing` | model | static | tests | In a test file, a scenario's elements are user-facing (`button`, `link`, `field`, `heading`, `content`; on mobile `button`, `field`, `content`). `testId()` is the one escape hatch, and the line STATES what the element lacks: a `// testId: <what is missing>` comment on the call's own line or the one directly above. It is an invariant, not a rationale. | Write `// testId: <no accessible name \| no role \| ...>` on the call's line or the one above, or name the element with `button()`/`link()`/`field()`/`heading()`/`content()`. | Testing what the user sees (roles, labels) keeps specs robust to DOM rewrites; a test id sidesteps that guarantee, and without the invariant written down nobody can tell whether the hatch is still needed. |
|
|
306
|
+
| <a id="target-matches-one-element"></a>`target-matches-one-element` | model | runtime | tests | A verb acts on exactly ONE element: a target that matches several is refused, never settled by the first. An element target must designate exactly ONE element when a verb ACTS on it (`click`/`tap`/`fill`): if several match, the action is refused with an error enumerating the candidates and offering the rewrites (`within(...)`, another target). The framework never acts on "the first". A name designates the WHOLE accessible name (`{ exact: false }` is the opt-out), and a target that finds nothing whole is not found, whatever a substring would have matched. It holds on both scenario specifications, website and mobile; on mobile, `see()`, which acts on nothing, is satisfied by any visible match (the XCUITest tree legitimately duplicates a label between a container and its child). | Scope it with `within(<landmark>, ...)`, or name the element with a target that designates one thing. | Acting on the first match makes a spec green while the visitor clicks something else, and nothing ever says so: an ambiguity is a writing fault, not a case to be settled by DOM order. The counting exists only at run time: no static analysis can do it, which is why the channel is runtime. |
|
|
307
|
+
| <a id="no-landmark-on-mobile"></a>`no-landmark-on-mobile` | mobile | runtime | tests | An iOS screen has no ARIA landmark: a mobile verb is scoped with `within(testId(...), ...)`. The visitor's vocabulary is ONE between the website and mobile specifications; an ARIA landmark (`main()`, `navigation()` and the like) passed to a mobile verb is refused at run time. | Scope with `within(testId(...), ...)`: a screen has no landmarks, and any target works as the scope. | One vocabulary keeps the API memorable; refusing the part with no iOS equivalent outright avoids a silent approximation. |
|
|
308
|
+
| <a id="verb-belongs-to-its-specification"></a>`verb-belongs-to-its-specification` | model | type | tests | The verb set is the boundary between the specifications: `goto` is the page's, `rerender`/`unmount` the component's, `tap` the screen's, and `focused`/`disabled`/`selected`/`valued` reach only where `see` and `gone` do. | Reach for the verb the specification offers; a missing one is a decision, not an omission. | One vocabulary across three specifications is memorable only while each specification refuses the words it cannot mean. |
|
|
309
|
+
|
|
310
|
+
## imports: Who imports whom, and protecting production
|
|
311
|
+
|
|
312
|
+
| Rule | Specification | Channel | Reach | Convention | Fix | Rationale |
|
|
313
|
+
| ------------------------------------------------------------------------------- | ------------- | --------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| <a id="no-test-code-in-production"></a>`no-test-code-in-production` | model | static | all | A production module never reaches test material. A production module is every source module that is no test material itself (`src/`, `lint/`, `bin/`, `scripts/` and a project's root modules alike) but a tool's config (`*.config.*`) and a module under a `__tests__/` folder or a separate test root (a root `test/` or `tests/`, `test-in-its-tests-folder`'s); test material is a test package (`@jterrazz/test`, `vitest`, `@vitest/*`, `vitest-mock-extended`, `vitest-browser-*`, `msw`, `@testing-library/*`, `playwright`, `playwright-core`, `@playwright/test`), a `*.test.*`, `*.test-d.*`, `*.spec.*`, `*.specification.*`, `*.fixtures.*` or `*.render.*` module, anything under a `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` folder or under `specs/`, and a stub: a `*.stubs.ts` facade (an entry at the source root, `src/index.stubs.ts`, is none) and the `stubs/` folder that holds one. Every form reaches: an import, `import type`, a re-export (`export *` included), `import()`, `require()`, `require.resolve()`, `import.meta.resolve()`, a type's `import('...')`; a specifier resolving there through a tsconfig `paths` alias (its `extends` chain followed) or a `#...` entry of the package's `imports`; a path the module builds from where it sits (`new URL('...', import.meta.url)`, `join(import.meta.dirname, ...)`, a template opening on `__dirname`) or reads (`readFileSync('./...')`); and a module that is not production reaching it in turn, in any of these forms, read from its parsed program: a production module on the way is judged for its own references. The four ground names mean ground wherever a test stands on a file, in a module's `__tests__/` as much as under `specs/`. Two exceptions: `@jterrazz/test/oxlint`, the zero-runtime lint entry; and a test package the unit's `jterrazz.config.ts` names under `test.production.allow`, each with the reason its product runs it (`{ playwright: 'the gate drives a browser in production' }`); an entry with no reason leaves the list unread. | Move the code that needs the reference into a test, move what production needs out of the test material, or inject it as a port. | Keeping test artefacts out of production protects the consumer's application bundle. |
|
|
315
|
+
| <a id="service-or-stub-from-its-subpath"></a>`service-or-stub-from-its-subpath` | model | static | all | A service or a stub is imported from its subpath, never from the root: `postgres`, `redis`, `sqlite`, `process`, `ProcessHandle` and the types of a service, a database or a container from `@jterrazz/test/services`; `anthropic`, `openai`, `http`, `defineStub`, `defineStubs`, `intercept` and the types of a stub, a filter or an interception from `@jterrazz/test/stubs`. An import or a re-export from `@jterrazz/test` naming one is an error at the name, and so is a member of a namespace import of the root (`t.postgres`). The fix moves each name to its subpath, a type-only import staying type-only; a namespace import is left to its reader. | Import the name from its subpath, `@jterrazz/test/services` or `@jterrazz/test/stubs`; `typescript fix` moves a named import, and a namespace import is rewritten by hand. | The root is what writes a spec. Filed at their subpaths, the services a spec starts and the stubs it serves are read off its imports, and a test that imports the root for a `file()` loads no service constructor and no stub builder. |
|
|
316
|
+
| <a id="fixtures-imported-by-tests-only"></a>`fixtures-imported-by-tests-only` | model | toolchain | all | A `*.fixtures.ts` is importable only from a test file. | Move the value into production code if production needs it; otherwise import it from a test. | Confining fixtures to tests stops test data leaking into production code. |
|
|
317
|
+
| <a id="no-second-test-runtime"></a>`no-second-test-runtime` | model | toolchain | tests | A test file imports no second test runtime: `@testing-library/*`, `happy-dom`, `jsdom`, `vitest/browser`, `vitest-browser-*`, `@vitest/browser*`, `msw`, `msw/*`, `nock`, `sinon`, `vitest-mock-extended`, `jest`, `@jest/*`, `mockdate`, `playwright`, `playwright-core`, `@playwright/test`, `webdriverio` and `appium` are errors, one layer per seam (`no-second-test-runtime-<seam>`), each named with what replaces it; `react-dom/server` is one too, in the `module` role alone. Reach: the roles that RUN a test (`module`, `component`, `spec`, `specification`); a fixture project's config and a providers module under `specs/` legitimately name the adapter. | Go through what replaces the seam: a specification, the visitor's vocabulary, `intercept()`, `clock` or `mockOf`. | A test importing the adapter speaks the adapter's dialect: a repository ends up with as many vocabularies as it has seams. Every group names the seam this vocabulary already owns: the runner, the browser, the simulator, the network, the clock and the doubles. |
|
|
318
|
+
| <a id="no-test-to-test-import"></a>`no-test-to-test-import` | model | toolchain | tests | A test file never imports another test file. | Under `specs/`, move what the two read into the `__setup__/` of the lowest folder they share; for a module test under `src/`, into a `__tests__/<file>.fixtures.ts` for typed input data. | Sharing between tests goes through a module that prepares them (a `__setup__/` module under `specs/`, a `__tests__/<file>.fixtures.ts` under `src/`), not through test-to-test imports that couple the files. |
|
|
319
|
+
| <a id="no-unpublished-subpath"></a>`no-unpublished-subpath` | model | toolchain | all | An import of `@jterrazz/test/<subpath>` is an error unless the package's `exports` map publishes the subpath: `./services` and `./stubs` for a spec, beside the root, and `./vitest`, `./config`, `./oxlint` and `./schema` for the tools. The list is READ from the manifest, never copied into the rule, so a published entry is exempt the day it ships; an unpublished subpath is the import the cruise cannot resolve through the `exports` map. | Import the name from the entry that publishes it: `@jterrazz/test`, `@jterrazz/test/services` or `@jterrazz/test/stubs`. | A closed list of entries keeps the public API explicit and the internal modules invisible. |
|
|
320
|
+
| <a id="no-redundant-test-dependency"></a>`no-redundant-test-dependency` | model | checker | member | A member declares no dependency `@jterrazz/test` already carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only, as a production dependency it is the product's own library) and no retired seam (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`), except `@testing-library/react-native` where jest runs the tests of the member: the only vocabulary that runtime has until the React Native answer lands. Jest runs them when the member DECLARES it (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it) or when a `jest.config.*` elsewhere in the workspace ROOTS into the member, which is the shape a library co-locating its render tests takes when the app beside it owns the only working toolchain. The optional peers (`playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers` and the like) are declared by the consumer, by design. | Remove the declaration: a transitive is already resolved by the package; a retired seam has a specification that replaces it (`component()`, `website()`, `clock`, `intercept()`). The diagnostic carries a sentence for each case. | A duplicate declaration lets two versions of one seam coexist, and a retired seam keeps a vocabulary the framework has replaced. |
|
|
321
|
+
|
|
322
|
+
<!-- /GENERATED:catalogue -->
|
|
323
|
+
|
|
324
|
+
A convention with no oxlint rule is held by another channel, and the Channel column above names which.
|
|
325
|
+
|
|
326
|
+
A static rule that probes the filesystem anchors on a file oxlint is already visiting (a feature's sibling `__requests__/`, `__seeds__/`, `__expected__/`, a module test's neighbour), with per-run caching (the kit's `readFileCached` and `listDirectory`); it never scans the tree blindly.
|
|
327
|
+
|
|
328
|
+
## What the preset carries
|
|
329
|
+
|
|
330
|
+
Which module may import which, and where a file or a folder may sit, are questions `@jterrazz/typescript` already answers with dependency-cruiser and ls-lint, so the rules of this catalogue that ask them are handed to it rather than run by code of this package. Everything this package holds a project to travels in ONE preset, `test()` from `@jterrazz/test/config`: the defaults of the `test` section, its oxlint plugin and rules, those import and naming rules, and the section of `.jterrazz/baseline.json` it owns, `test`, the checker's debt. A project lists it in `jterrazz.config.ts`, as [Set up a project](../start/set-up-a-project.md#turning-the-conventions-on) writes.
|
|
331
|
+
|
|
332
|
+
How the presets merge, and how a project sets one of their laws aside, is `@jterrazz/typescript`'s: [Configuration § Presets](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/configuration.md#presets). This repository is held the same way: its `oxlint.config.ts` is the derived one-liner, and its `jterrazz.config.ts` lists the `test()` of the build the package just made, beside its own overrides.
|
|
333
|
+
|
|
334
|
+
Its rules are the catalogue's `toolchain` channel. The import rules print as `architecture/<rule>` at the import, from the toolchain's Architecture (layer map) pass (`no-second-test-runtime` as one layer per seam it guards, `architecture/no-second-test-runtime-network` for `msw`, each teaching what replaces its package); the name rules print as `names/<rule>` at the file or the folder, from its Names (tree) pass; every one in four parts, its `read` the row of this catalogue. A project that declares no preset runs none of them: a preset is declared, never discovered ([Configuration § Presets](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/configuration.md#presets)). Their debt is the toolchain's ratchet, `.jterrazz/baseline.json › typescript`, recorded by `typescript baseline`.
|
|
335
|
+
|
|
336
|
+
- The oxlint half of `test()` is `{ jsPlugins: ['@jterrazz/test/oxlint'], rules: recommendedRules, overrides }`. Its `overrides` relaxes `import/exports-last` for `**/*.specification.ts`: the `cleanup-passed-to-after-all` idiom (`export const { cli, cleanup } ... ; afterAll(cleanup)`) legitimately ends a spec file on a non-export statement, so the relaxation ships here instead of being hand-rolled in every strict consumer. The same idiom exports a `cleanup` used only inside its own file, which knip would report as unused; the toolchain's knip preset already sets `ignoreExportsUsedInFile`, so nothing is written for it.
|
|
337
|
+
- `recommendedRules` (also exported standalone) enables the whole catalogue in one spread, every rule at `error`. There is no warn tier: a rule errors, its debt held by the project's ratchet, or it goes; oxlint's `.jterrazz/oxlint-suppressions.json` holds errors alone, so a warning would be debt nothing holds down.
|
|
338
|
+
- Wiring is **explicit**: the preset never auto-detects `@jterrazz/test` from your dependencies. A project lists `test()` in `jterrazz.config.ts`, and its derived `oxlint.config.ts` reads the presets from there; a project that does not runs no `jterrazz/*` rule. When and how `typescript check` decides to RUN the checker is the toolchain's, not this page's: [Quality checks § The Test Conventions pass](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/quality-checks.md#the-test-conventions-pass).
|
|
339
|
+
- `@jterrazz/test/oxlint` is **tool-facing only**: named by the `test()` preset for the linter to load, never imported by test or production code. It is the one specifier exempt from rule `no-test-code-in-production`, a zero-runtime lint entry, so a preset that wires the plugin may import it from anywhere. `no-unpublished-subpath` exempts every subpath the package's `exports` map publishes, this one among them.
|
|
340
|
+
- `materialAt(path, directory?)` and `testPackageOf(specifier)` (typed, with `MaterialKind`) are the reader `no-test-code-in-production` judges by, published for a build guard: the first names what a module id is (`'a test file'`, `'ground'`, `'the specs tree'`, `'a stub'`) or answers `undefined`; the second names the test package a bare specifier belongs to (`vitest/config` belongs to `vitest`), this tool entry aside. They are published so a build guard (the toolchain's Publish pass) reads what test material is from its one owner, never a copy.
|
|
341
|
+
- **The oxlint version is the toolchain's to pin.** `@jterrazz/typescript` owns the range every profile runs the binary at; a repository whose own code imports oxlint (this one does, from every rule test, through `oxlint/plugins-dev`) declares `oxlint` in its `devDependencies` at exactly that range, so the line here MIRRORS the pin instead of holding a second opinion about it and a plain `npm ci` hoists one oxlint rather than two. `lint/self/corpus/__tests__/oxlint-pin.test.ts` fails the day the two diverge.
|
|
342
|
+
|
|
343
|
+
## The conventions checker
|
|
344
|
+
|
|
345
|
+
oxlint reads JavaScript and TypeScript only, and some conventions are about other files (the `{{token}}` grammar of `__requests__/` and `__expected__/`, the `<case>.spec.yaml` documents) or about several files at once. Those are the **conventions checker**: the checks the `test()` preset names, which `typescript check` runs as its Test Conventions pass, `typescript fix` before its formatter, and `typescript baseline` to record its section. It is no binary of its own (the toolchain runs it through its checker program), and that is the road a run by hand takes too:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
typescript check # every pass, the conventions among them, over the whole project
|
|
349
|
+
typescript fix # the conventions' fixes first, then oxlint's and the formatter
|
|
350
|
+
typescript baseline # records .jterrazz/baseline.json › test beside the toolchain's sections
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Those three are the whole surface: the checker's own command line is the toolchain's to drive, never a gesture a project types ([Test](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-checkers-command-line) keeps what it reads, for this repository's maintainers).
|
|
354
|
+
|
|
355
|
+
**It is the rules kit's checker** (`runChecker` of `@jterrazz/typescript/rules`, the conventions pass and the `baseline` command handed to it), and **it prints the line oxlint's findings normalise to**, the compiler convention every editor's problem matcher parses: one line per finding on stdout, the message last:
|
|
356
|
+
|
|
357
|
+
```text
|
|
358
|
+
specs/api/orders/create.spec.ts:7:5: error jterrazz-check(no-reach-into-sibling-ground): found: "../users/__fixtures__/x/" leaves specs/api/orders · why: … · fix: … · read: docs/reference/linting.md#no-reach-into-sibling-ground
|
|
359
|
+
specs/api/__requests__/x.http:3:1: error jterrazz-check(no-token-in-request): found: token {{uuid}} in a __requests__/ file · why: … · fix: … · read: docs/reference/linting.md#no-token-in-request
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The code is the pass's name, so it is the key a suppression names and the ratchet counts. A finding about a whole file or folder points at `:1:1`. A check spots what it finds and teaches it (the position, what was found, the fix for this case), and the pass makes each finding with the kit's `defineCheck(row).finding(...)`, the row giving the why and the read. A spec proves a finding with `specification.lint()` ([Lint specs](../guides/lint-specs.md)): its report reads oxlint's `unix` lines and these alike, heads each finding `file:line:column jterrazz-check(<code>)` and writes its lesson a part per line below it, and drops every other line: a tally, a summary. It states oxlint's format itself, `--format=unix`: without one, oxlint renders graphical frames outside an agent's shell (CI included), and the report refuses them rather than read them as a clean run.
|
|
363
|
+
|
|
364
|
+
A run judges the whole project: every `specs/` tree the workspaces declare below its root, plus every member. The tree passes read the first level of a tree (`file-suffix-matches-place`, `specification-test-uses-its-runner`, `specification-folder-has-its-runner`), so the run is anchored on each specs tree the project declares.
|
|
365
|
+
|
|
366
|
+
What it checks:
|
|
367
|
+
|
|
368
|
+
- **`no-malformed-ref`:** every `{{token}}` in an `__expected__/` fixture belongs to the frozen vocabulary. It scans **all text files** under `__expected__/` (not just `.http`/`.json`/`.txt`; it decodes UTF-8 and skips binary), and flags **malformed captures** of a known kind (`{{iso8601#}}`, `{{uuid #id}}`).
|
|
369
|
+
- **`http-first-line-follows-its-grammar`:** depth-1 `__requests__/*.http` must start with a request line (`METHOD /path`); depth-1 `__expected__/*.http` with a status line (`HTTP/1.1 <status>`).
|
|
370
|
+
- **`no-token-in-request`:** a `{{token}}` inside a `__requests__/` file: requests are inputs, never matched, so a token there is almost always a mistake.
|
|
371
|
+
- **the tree passes:** what a folder says about itself, which no single file can answer for: a specification folder holds the runner its name promises (`specification-folder-has-its-runner`), a spec under it reaches that runner (`specification-test-uses-its-runner`), a `.test.ts` under a specification folder wears the wrong word and a `.spec.ts` outside every specs tree wears the other one (`file-suffix-matches-place`, fixable), a document is never filed under ground (`no-document-in-ground`), and ground a single spec of a leaf reads belongs beside that spec (`ground-beside-its-only-reader`).
|
|
372
|
+
- **`checker-suppression-states-a-reason`:** every `checker-disable-next-line` / `checker-disable-line` carries its `-- <reason>`. The toolchain's own suppression gate reads `oxlint-disable*` and nothing else, so this channel carries its own.
|
|
373
|
+
- **`checker-suppression-names-a-pass`:** every id such a directive names is a pass of the checker, or `*`: a misspelled id, or one a major retired, silences nothing and is reported at the directive.
|
|
374
|
+
|
|
375
|
+
It shares `TOKEN_KINDS` with the runtime matcher (the two cannot drift), skips `__fixtures__/` trees (verbatim `.fixture()` cwd material: file state, not assertions), and ignores non-identifier braces (`{{.Server.Version}}`-style template output). The exit code is the ratchet's ([below](#the-ratchet-the-test-section-of-jterrazzbaselinejson)): with no `test` section in `.jterrazz/baseline.json`, 1 on any finding and 0 on none.
|
|
376
|
+
|
|
377
|
+
**`typescript fix` runs the conventions' fixes FIRST and its formatter LAST.** The checker's fixers write code (a moved helper, a view declared once, a value written in place), and none of it is formatted until the formatter runs over it, so the toolchain runs a preset's fixes before its own ([Quality checks § A preset's checks](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/quality-checks.md#a-presets-checks)), and a repository's `typescript fix` is that one line, `npx typescript fix`.
|
|
378
|
+
|
|
379
|
+
What it walks is what git does NOT ignore, like every tree gate of the toolchain: inside a work tree, one `git ls-files --cached --others --exclude-standard` from its top level answers every walk (tracked files, and untracked ones no `.gitignore` names), so a new spec is judged the moment it is written, before anyone runs `git add`, and a `.gitignore`d path (a local data folder belongs there) is never reported, on the workstation that holds it or anywhere else. A directory git places in no work tree (`git rev-parse` fails there) is walked on disk, which is where a copied fixture or a scratch tree is read. The listing judges the project's OWN files: a path inside a `node_modules/` is a dependency's and is read on disk, whatever `.gitignore` says, so `no-unpublished-subpath` reads the `exports` of an installed `@jterrazz/test`, which every consumer's `.gitignore` hides. A listing answers only while it is newer than git's index and than the directory it is asked about, so an editor that keeps the plugin loaded between two runs sees a file written between them: oxlint marks no run boundary a plugin could reset on.
|
|
380
|
+
|
|
381
|
+
The same listing answers every single path a pass probes (`named-path-exists`'s target, `no-unused-ground` and `ground-shared-or-inline`'s pool entry, the member pass's manifests and workspace anchors, the package a walk stops at, the fixture `no-volatile-literal` reads) and every file the plugin's disk-anchored rules read, the neighbour `test-in-its-tests-folder` looks for and the stray `seeds-are-sql` refuses alike, and the root `no-redundant-root` derives, which the runtime derives from the same listing: an ignored file neither satisfies nor violates a rule. The one path read on disk is a repository's own `.git`, which git never lists.
|
|
382
|
+
|
|
383
|
+
`typescript fix` applies the rewritable passes and writes every spec document in [its format](../guides/spec-documents.md#the-schema-and-the-format), then checks what is left. A document out of its format is no finding and no baseline holds it: a check run names it and fails, as oxfmt's check does for a source file.
|
|
384
|
+
|
|
385
|
+
## The member pass
|
|
386
|
+
|
|
387
|
+
Two findings are about a MEMBER rather than about a spec tree, and a member with no `specs/` root has to be judged all the same. A member's `vitest.config.ts` is no longer one of them: it is the line `jterrazz.config.ts` derives, which `@jterrazz/typescript`'s Config pass writes and holds.
|
|
388
|
+
|
|
389
|
+
They are [`no-simulated-dom-in-config`](#no-simulated-dom-in-config) (a test project of `jterrazz.config.ts` naming a simulated DOM) and [`no-redundant-test-dependency`](#no-redundant-test-dependency) (a dependency the framework carries, or a seam it replaced); each row says what it finds.
|
|
390
|
+
|
|
391
|
+
A workspace ROOT is a member like any other, and its tests are only those that are its own: the walk stops at the next `package.json`, and a `test` script that hands the work to the members (`--workspaces`, `pnpm -r`, `turbo run`) is the members' tests, not the root's. `yaml` is read from devDependencies alone: in `dependencies` it is the product's own YAML library, which the framework never ships to it.
|
|
392
|
+
|
|
393
|
+
`no-redundant-test-dependency` says which of the two it found: a carried dependency is "a transitive of `@jterrazz/test`", whose fix removes the declaration, and a retired seam is "a seam `@jterrazz/test` replaced", whose fix names the specification that took its place (`component()`, `website()`, `clock` or `intercept()`). Removing a transitive changes nothing a test can see; dropping a retired seam means writing its specification.
|
|
394
|
+
|
|
395
|
+
**The jest allowance follows the tests, not the manifest.** `@testing-library/react-native` stands where jest runs the member's tests, and jest is not always the member's own: a library co-locating its render tests in `src/` while the app beside it owns the only working React Native toolchain runs them from THAT app's `jest.config.*`. So the pass reads two sources: the member's own declaration (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it), and a `jest.config.*` anywhere in the workspace whose `roots`/`testMatch` resolve INTO the member (`<rootDir>` is jest's own token for the config's directory). A config that reaches another member clears nothing here: the allowance is about the files jest collects.
|
|
396
|
+
|
|
397
|
+
The workspace it searches is walked UP to, from the member, and never taken from the anchor the run was started at: the app that runs the member's tests sits BESIDE it, which is above the member and never below it. A run anchored on the member (the run its own gate makes) otherwise found no jest at all and reported `no-redundant-test-dependency` while the root-anchored run cleared the same member. The walk stops at the first directory declaring a workspace (`workspaces` in its manifest, or a `pnpm-workspace.yaml`) and at the checkout's own boundary, since a `.git` is where this repository ends.
|
|
398
|
+
|
|
399
|
+
The member pass also carries the two findings a tree walk of the member makes: `no-snapshot-folder` (a folder named for goldens in another word) and `named-path-exists`'s module half: a `file()`/`directory()` in a module test under `src/` or `lint/` names an entry that exists, as surely as one in a spec.
|
|
400
|
+
|
|
401
|
+
`typescript check` runs both halves over the whole project: every `specs/` root discovered from the root manifest's `workspaces`, and every member. That is the run the ratchet rests on ([below](#the-ratchet-the-test-section-of-jterrazzbaselinejson)), so the contract is exactly "the run over the whole project reports what a run over each root and each member would", and the discovery is the toolchain's, to the segment: a `workspaces` glob expands with `*` for one segment and `**` for any depth (bounded at six), and each member is walked for the spec roots it owns, a nested `web/specs` included. A shape `@jterrazz/typescript` reaches and this run did not would be a root its Test Conventions pass runs and the ratchet never counts.
|
|
402
|
+
|
|
403
|
+
A workspace whose ROOT lists no `test()` preset, every member listing its own, is judged member by member: the toolchain runs the checker once per declaring member (`--member <dir>`), and that run judges the member's own spec trees with its member passes. Where the root lists the preset, the root's run already judged every tree, and a member's run judges the member alone, so no tree is judged twice, and none is left to a pass that printed `✓ Passed` over nothing, as the OS's did before.
|
|
404
|
+
|
|
405
|
+
### The ratchet: the `test` section of `.jterrazz/baseline.json`
|
|
406
|
+
|
|
407
|
+
This package records and judges its consumers' debt. The family keeps one tracked ratchet file, `.jterrazz/baseline.json`, with a section per preset (`{ "typescript": {...}, "test": {...} }`), and the `test()` preset owns one, `test`, this section. It is read, written and judged by the rules kit of `@jterrazz/typescript`, the same engine that judges the `typescript` section, so the refusals read alike: each names its gesture: `typescript baseline` to record, `typescript fix` to re-record a fall. This repository holds none: its own tree carries no debt to record.
|
|
408
|
+
|
|
409
|
+
#### The section
|
|
410
|
+
|
|
411
|
+
- **Its keys** are `{ "<key>": count }`, `jterrazz-check/<name>` for every checker finding. The plugin's `jterrazz/<rule>` diagnostics are oxlint's own, held file by file in `.jterrazz/oxlint-suppressions.json` beside every other oxlint rule (`typescript baseline` records them, `typescript fix` prunes a fall), so a section still holding a `jterrazz/<rule>` key is refused, naming the gesture that moves it: `typescript baseline`, which records both files. One finding, one file, one judge.
|
|
412
|
+
- **It is EXACT.** An entry states where the project stands, never a ceiling, so a count below its entry breaks it as an entry at zero does.
|
|
413
|
+
- **`typescript baseline` writes it** from where the project stands, and leaves every other section byte for byte. A count that rose and a key with no entry are written by `baseline` alone, never by a fix.
|
|
414
|
+
- **`routes/unreached` is the run's key.** The api run counts the routes and alone judges it, as exactly ([API specs § Route coverage](../guides/api-specs.md#route-coverage)); its one recorder is a test run under `TEST_UPDATE=1`, and the checker's recordings (`typescript fix` included) keep it as the run left it, even when the count fell.
|
|
415
|
+
|
|
416
|
+
#### What `typescript fix` does to it
|
|
417
|
+
|
|
418
|
+
- **It re-records both falls** (lowering an entry or dropping it) before it judges.
|
|
419
|
+
- **It runs the plugin's own autofixes first**, after its own rewrites and movers: the project's oxlint, under a wrapper of its root config that arms the `jterrazz/*` rules alone, in that config and in every config it extends: a plugin armed through `extends` keeps its fixes, and a JSON config, `.jsonc` or extended, is read as oxlint reads it, comments and trailing commas allowed. So no other rulebook's fixer writes a byte, and the ratchets count the checker's findings and the plugin's diagnostics over the one tree they leave: one `typescript fix` settles a diagnostic its fix writes (`no-repeated-chain-setting`'s view, `one-describe-per-file`'s describe, `expected-constant-shared`'s shared expectation).
|
|
420
|
+
- **A nested oxlint config gets a run of its own**, under the same wrapper of ITS config: a config handed with `-c` turns oxlint's nested discovery off, so the root's run leaves the subtree alone rather than write where another config governs, and the subtree's run fixes it by the config the ratchet's count judges it by.
|
|
421
|
+
- **A config oxlint could not load under the wrapper stops `typescript fix`**, naming it: oxlint exits 1 for it as for diagnostics left, and a pass that read it as the latter would have fixed nothing and said nothing.
|
|
422
|
+
|
|
423
|
+
#### Which run owns the verdict
|
|
424
|
+
|
|
425
|
+
ONE run owns it: the run over the whole project, the one `typescript check` always makes. The run the toolchain makes again in a member that declares presets of its own prints what it found and leaves the verdict to that run, naming the file that judges it relative to the project, `judged by .jterrazz/baseline.json › test`.
|
|
426
|
+
|
|
427
|
+
It counts the project's checker findings; oxlint judges the plugin's diagnostics itself, against its suppressions file, in the Oxlint pass. It fails on a count above its entry, a key with findings and no entry, a count below its entry, an entry at zero, or a `jterrazz/<rule>` key left from before oxlint held its own.
|
|
428
|
+
|
|
429
|
+
A project with no `test` section fails on any finding, and says in four parts, beneath the findings, the gesture that records them.
|
|
430
|
+
|
|
431
|
+
#### What it prints
|
|
432
|
+
|
|
433
|
+
- **Green, exactly one line:** `.jterrazz/baseline.json › test: 106 held across 7 rules`, every entry the section holds, the run's `routes/unreached` included, or `.jterrazz/baseline.json › test: nothing held` when it holds none (a project with no section included), whether the run checked, fixed or judged the root member. It is the form `typescript check` shows beside its own ratchet's; the held findings are not repeated on every run. A fall `typescript fix` re-records is named above it, `re-recorded .jterrazz/baseline.json › test › <key> <was> → <now>`.
|
|
434
|
+
- **Failing**, every checker finding it counts, before the reasons, so a failing verdict and its evidence are one log. The plugin's diagnostics are oxlint's to print, in the Oxlint pass.
|
|
435
|
+
|
|
436
|
+
The toolchain's side of this (when the pass runs, which install it resolves the checker from, and what the floor version is) is `@jterrazz/typescript`'s [Quality checks § The Test Conventions pass](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/quality-checks.md#the-test-conventions-pass).
|
|
437
|
+
|
|
438
|
+
## Cross-file checker passes
|
|
439
|
+
|
|
440
|
+
The same checker also runs the analyses oxlint structurally cannot: those that need to read **two files at once** (a `*.specification.ts` record and the `specs/` tests importing it) or a **whole feature tree** at once. They live in the checker channel and follow its doctrine: **precision over recall**. A heuristic that cannot decide stays silent (under-reports) rather than block a legitimate shape. All are string/scan based (no TS parser in the light lint bundle). The specs root's shared ground (`<specs>/__fixtures__/`, `<specs>/__seeds__/`, `<specs>/__requests__/`, `<specs>/__expected__/`) is pruned from the per-feature walks (verbatim material, the on-purpose violation trees included) and inspected only at its top level.
|
|
441
|
+
|
|
442
|
+
- **`reserved-folder-in-closed-list`**, **`spec-code-in-setup-folder`**, **`setup-module-at-lowest-common-folder`**, **`adapter-named-for-its-port`:** the shape of a specs tree [Conventions § The naming recap](../concepts/conventions.md#the-naming-recap) states: a root of specifications, the closed list of special folders, code in `__setup__/`, a shared module at the lowest folder its importers share, a test adapter named for its port. They read the tree and its imports, never a test's body.
|
|
443
|
+
- **`no-unused-ground`:** the mirror of `named-path-exists`: a fixture file no test literal references is dead weight, and a folder holding ground with no test at or below it is an orphan. A referenced `__expected__/<name>/` (or `__fixtures__/<name>/`) tree counts entirely as used; an entry of the shared ground unreferenced across all of `specs/**` is dead. A template literal with an expression is read as a pattern (its folder, the text before the hole and the text after it), and every entry it can reach counts as referenced; a path built any other way (a variable, `resolve(DIR, name)`) names no entry, so what only it reaches is reported. Nothing is ever downgraded: the rule errors like every other.
|
|
444
|
+
- **`docker-exec-bound-with-await-using`:** reads the docker-aware runners from the `docker:` option of the imported specification file and flags every `<runner>.exec()` result bound without `await using`.
|
|
445
|
+
- **`database-named-only-when-several`:** a spec's `services:` record fixes how many SQL databases exist; the importing tests must (≥ 2) or must not (== 1) pass `{ database }` to every `.seed()` / `.table()`. Non-literal records (a handle variable in `services`) and files importing more than one enforced runner are skipped (ambiguous). Doubled by the runtime channel (the framework throws).
|
|
446
|
+
|
|
447
|
+
### What the toolchain judges, and what stays here
|
|
448
|
+
|
|
449
|
+
A rule that only says where a name may sit, or which module may import which, belongs to the declarative tool the toolchain already runs rather than to code of our own. Every such check of this package was measured against ls-lint and dependency-cruiser, and the ones the tool expresses faithfully ride on [the preset](#what-the-preset-carries).
|
|
450
|
+
|
|
451
|
+
| Rule | Judged by | What it expresses |
|
|
452
|
+
| -------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
453
|
+
| `specs-root-holds-specifications-and-shared` | ls-lint | the whole rule: every file at a specs root but a test, a spec, a document or a runner |
|
|
454
|
+
| `special-folder-in-closed-list` | ls-lint | the `_` names outside the closed list of special folders (earlier spellings and unknown `__<word>__/` included) below every specs root |
|
|
455
|
+
| `spec-file-in-a-specs-tree` | ls-lint | a `.spec.ts` in any folder outside every `specs/` tree, `file-suffix-matches-place`'s first clause |
|
|
456
|
+
| `no-test-to-test-import` | dependency-cruiser | the whole rule: a test path importing a test path, resolved |
|
|
457
|
+
| `fixtures-imported-by-tests-only` | dependency-cruiser | the whole rule: a `*.fixtures.*` module imported from a path that is no test |
|
|
458
|
+
| `no-second-test-runtime` | dependency-cruiser | the whole rule, one layer per seam, each package matched installed under `node_modules/` or, not installed, by its bare specifier |
|
|
459
|
+
| `no-unpublished-subpath` | dependency-cruiser | the whole rule: an `@jterrazz/test/` import the cruise cannot resolve, since it resolves through the `exports` map |
|
|
460
|
+
|
|
461
|
+
Three limits come with the tools, and each is stated where it bites. ls-lint never judges a file at the root it runs from, so a `.spec.ts` at a project's own root stays `file-suffix-matches-place`'s. dependency-cruiser cruises the project's top-level folders, never a module at its root, so a root `vitest.config.ts` importing an unpublished subpath is no longer refused. And a cruise follows import edges alone: a `vi.mock('./x.fixtures.js')` string names a fixtures module without importing it, which the plugin rule used to refuse and the layer cannot see.
|
|
462
|
+
|
|
463
|
+
What stays custom is what neither tool can express:
|
|
464
|
+
|
|
465
|
+
| Rule | Why it stays |
|
|
466
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
467
|
+
| `no-snapshot-folder` | A deny-list (`__snapshots__`, `golden`, `goldens`, `snapshots`) RE2 cannot write without a negative lookahead; outside `specs/`, the folder is refused only when it holds no code, which ls-lint cannot read. |
|
|
468
|
+
| `reserved-folder-in-closed-list` | Its remaining clauses read what a folder holds: a special folder spelt without its marks, such as `fixtures/`, or a `shared/`, is refused only when it holds no spec, and a `__setup__/` only when it holds data or a special folder. |
|
|
469
|
+
| `file-suffix-matches-place` | The root `.spec.ts` above; a `.test.ts` under a specification folder is judged by whether it reaches a runner, and a document's name by its words and its folder. |
|
|
470
|
+
| `no-test-code-in-production` (source) | Test material is not a path pattern: a `stubs/` folder is test material only where it holds a `*.stubs.ts` facade, a path a module builds or reads (`join(import.meta.dirname, ...)`, `readFileSync('./...')`) is no import edge, the test packages a unit allows run from its `test.production.allow`, and the cruise skips the ground folders this rule refuses an edge into. Its tarball half is the toolchain's Publish pass, which loads this package's readers from its `./oxlint` entry. |
|
|
471
|
+
|
|
472
|
+
The self checks' verdicts are [Test § The package's own rules](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-packages-own-rules).
|
|
473
|
+
|
|
474
|
+
## Pitfalls
|
|
475
|
+
|
|
476
|
+
- **Suppressing instead of restructuring.** Most hits have a conventional home: an inline runner belongs in a `*.specification.ts`, a helper several specs share in the `__setup__/` of the lowest folder they share, a file-reading module test in `specs/`. Suppress only what is structurally impossible (negative constructor specs).
|
|
477
|
+
- **Forgetting to declare the preset.** Oxlint passing does not validate `.http`/`.json`/`.txt` fixtures; list `test()` in `jterrazz.config.ts`, and `typescript check` runs the conventions with everything else.
|
|
478
|
+
- **Linting violation fixtures.** A tree that breaks the rules on purpose (like this repo's `specs/lint/**/__fixtures__/**`) is left out of the linter, by one of the toolchain's doors ([its Quality checks § Leaving a path out](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/quality-checks.md#leaving-a-path-out)).
|
|
479
|
+
- **A hand-written `oxlint.config`.** The plugin is ESM-only, and a CommonJS config would print a load warning, register nothing and exit 0. The derived `oxlint.config.ts` is ESM, and `typescript check` refuses any other content (`config/editor-file-derived`), so a project never writes one. The `./oxlint` entry's `require` condition serves a different reader: a module of your own that reads `recommendedRules` synchronously, `require('@jterrazz/test/oxlint')`.
|
|
480
|
+
|
|
481
|
+
## Related
|
|
482
|
+
|
|
483
|
+
[Set up a project](../start/set-up-a-project.md) · [Lint specs](../guides/lint-specs.md) · [Tokens](tokens.md) · [Conventions](../concepts/conventions.md)
|