@jterrazz/test 18.1.0 → 18.2.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.
@@ -79,7 +79,7 @@ Plain values take vitest's native matchers. No framework matcher exists (or is n
79
79
  | `await result.filesystem.files()` | `string[]` | `expect(await result.filesystem.files()).toContain('shoply.lock')` |
80
80
  | `await cli.run(file('./<case>.spec.yaml'))` | `CliResult` | the document asserts itself; the LAST run's result comes back; see [Spec documents](../guides/spec-documents.md#the-three-doors-one-runner) |
81
81
 
82
- ## `result.response`: HTTP response (api)
82
+ ## `result.response`: HTTP response (api, deployment)
83
83
 
84
84
  | Matcher | Sync/async | Example |
85
85
  | -------------------------------- | ---------- | --------------------------------------------------------------------------- |
@@ -105,6 +105,14 @@ Response status mismatch (./__expected__/user-created.http)
105
105
 
106
106
  A header mismatch has the same shape (`Response header mismatch (name)`, with `header:` / `expected:` / `received:` lines; a missing header shows `received: (absent)`). A body mismatch prints a line-by-line structural diff (`Response mismatch (name)`, `- Expected` / `+ Received`) of the expected body (with matchers and tokens rendered as their placeholder text, `{{uuid#order}}`) against the actual body: a JSON value as its pretty-printed lines, and a text body (when both sides are text) as its own lines, so an HTML page or an event stream reads as written. The diff is literal: it does not annotate captured ref values, and a failed `{{uuid#order}}` re-occurrence surfaces only as the two differing lines.
107
107
 
108
+ An exchange with a running deployment that never answered is the one line `unreachable: <reason>` ([Deployment specs § Unreachable is a result](../guides/deployment-specs.md#unreachable-is-a-result)), compared as the status line is, tokens allowed; against a status line, or another reason, the failure names both outcomes:
109
+
110
+ ```text
111
+ Response outcome mismatch (the value in place)
112
+ expected: unreachable: timeout
113
+ received: HTTP/1.1 200
114
+ ```
115
+
108
116
  `.not`: `expect(result.response).not.toMatch(file('./__expected__/user-created.http'))` passes when any part diverges. Rarely useful; prefer positive fixtures.
109
117
 
110
118
  ## `result.table(name, { database }?)`: database tables (api, job, cli, integration)
@@ -15,6 +15,7 @@ The words this manual uses, one meaning each.
15
15
  | seam | A place where the framework meets an outside tool it loads on demand: a database driver, a browser, a simulator. Each is an optional peer |
16
16
  | ratchet | A count of debt kept in `.jterrazz/baseline.json` that may only fall: an unreached route, a convention not yet followed. A run that raises it fails, and `TEST_UPDATE=1` records a fall |
17
17
  | specimen | A small sample app a testing tool keeps under `specs/__fixtures__/` to test itself against. Only a repository that tests a testing tool has one |
18
+ | unreachable | What an exchange with a running deployment reads when no response came (refused, timed out, unresolved, its certificate refused): the one line `unreachable: <reason>`, compared whole like a status line rather than thrown ([Deployment specs](../guides/deployment-specs.md#unreachable-is-a-result)) |
18
19
  | door | Where a test file may sit: in the `__tests__/` of its module's folder (a unit), under `specs/<kind>/` (an assembled product), or in a repository suite (the third door) |
19
20
 
20
21
  The examples use made-up products: `shoply`, a shop CLI; a news app with an events feed; and hosts under `.test` and `.example`, which never resolve on the internet.
@@ -153,7 +153,7 @@ Every rule a project using the framework meets, across its enforcement channels
153
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
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
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. |
156
+ | <a id="no-unknown-constructor"></a>`no-unknown-constructor` | model | static | all | Eight constructors and only eight: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.deployment()`, `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 eight, or reach for the `component` chain: a ninth subject is a decision before it is a constructor. | A closed surface stops parallel constructors being invented and keeps the API memorable. |
157
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
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
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. |
@@ -211,7 +211,7 @@ Every rule a project using the framework meets, across its enforcement channels
211
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
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
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. |
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`, `deployment`, `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
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
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
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. |
@@ -225,7 +225,7 @@ Every rule a project using the framework meets, across its enforcement channels
225
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
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
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. |
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 eight 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
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
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
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. |
@@ -251,7 +251,7 @@ Every rule a project using the framework meets, across its enforcement channels
251
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
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
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. |
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`, or `METHOD https://host/path` for a deployment) under `__requests__/`, a status line (`HTTP/1.1 <status>`, or `unreachable: <reason>` for an exchange that never answered) 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
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
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
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. |
@@ -145,6 +145,7 @@ await expect(result.table('payment_intents', { database: 'db' })).toMatch([
145
145
  - Segments already covered by a placeholder are **preserved**: a fixture line `Done in {{duration}}` stays `Done in {{duration}}` after an update, no matter what duration the run printed.
146
146
  - Values the framework knows to be dynamic are substituted automatically: the spec's cwd becomes `{{workdir}}` in the written fixture without you doing anything.
147
147
  - A golden that does not exist yet, or holds nothing but whitespace, is **recorded** from the output: a `.http`, a `.json` and a text golden alike, and a document's `response:` file too: a run that records stands without the goldens it is about to write. A golden is started by naming it, never by stubbing it.
148
+ - A recorded response golden states the status, the `content-type` and the body; a deployment's states the headers its specification records (`recordedHeaders`), keeps stating no body where it stated none, and an exchange that never answered is written as its one line, `unreachable: <reason>` ([Deployment specs § Update mode](../guides/deployment-specs.md#update-mode)).
148
149
 
149
150
  Example, starting from actual output:
150
151
 
@@ -14,7 +14,7 @@ Three things make a test suite expensive to keep, and the framework takes each o
14
14
 
15
15
  Five ideas cover everything else in the manual. They match the four families of the docs map, plus the files a test stands on.
16
16
 
17
- **1. A kind of test, and its runner.** What you test decides the kind: a module, a module against a real service (integration), a rendered component, a website, a mobile screen, an HTTP API, a job, a CLI, or the lint rules a project ships. Each kind except module and component has a constructor (`specification.api()`, `specification.cli()`, ...), called once per suite in a `*.specification.ts` file. It starts what the suite needs and returns a runner (`api`, `cli`, ...) and a `cleanup` you pass to `afterAll`. The pages call the code behind one kind a **specification**.
17
+ **1. A kind of test, and its runner.** What you test decides the kind: a module, a module against a real service (integration), a rendered component, a website, a running deployment, a mobile screen, an HTTP API, a job, a CLI, or the lint rules a project ships. Each kind except module and component has a constructor (`specification.api()`, `specification.cli()`, ...), called once per suite in a `*.specification.ts` file. It starts what the suite needs and returns a runner (`api`, `cli`, ...) and a `cleanup` you pass to `afterAll`. The pages call the code behind one kind a **specification**.
18
18
 
19
19
  **2. A chain: setups, then one action.** Every test is one chain on that runner: zero or more setups (`.seed()`, `.intercept()`, `.clock()`, `.env()` ...), then exactly one action (`.get()`, `.exec()`, `.visit()`, `.trigger()`, `.call()` ...), which resolves to a result. The chain reads as the test's sentence: the setups are the Given, the action is the When.
20
20
 
@@ -157,22 +157,23 @@ Ask the questions in order: the first yes is the kind, and the kind decides wher
157
157
 
158
158
  <!-- GENERATED:fork. Do not edit by hand; run `npm run docs`. Source: lint/consumer/catalogue/fork.ts -->
159
159
 
160
- | Question | Kind | Lives at | Constructor | Project | Read |
161
- | ---------------------------------------------------------------------- | ---------------- | ------------------------------------------------ | ----------------------------- | ------------- | -------------------------------------------------------------------------------------------- |
162
- | Does it render in a browser as a whole served page? | website | `specs/website/<domain>/<aspect>.spec.ts` | `specification.website()` | `website` | [Website specs](../guides/website-specs.md) |
163
- | Does it render as a component (React, a DOM function, a hook)? | component | `__tests__/<file>.test.tsx` by `<file>.tsx` | none, the `component` chain | `component` | [Component specs](../guides/component-specs.md) |
164
- | Does it render on a simulator? | mobile | `specs/mobile/<domain>/<aspect>.spec.ts` | `specification.mobile()` | `mobile` | [Mobile specs](../guides/mobile-specs.md) |
165
- | Does it answer HTTP? | api | `specs/api/<resource>/<action>/<aspect>.spec.ts` | `specification.api()` | `api` | [API specs](../guides/api-specs.md) |
166
- | Is it triggered by name, in-process? | job | `specs/job/<job>/<aspect>.spec.ts` | `specification.job()` | `job` | [Job specs](../guides/job-specs.md) |
167
- | Is it a binary? | cli | `specs/cli/<command>/` (+ `<case>.spec.yaml`) | `specification.cli(bin)` | `cli` | [CLI specs](../guides/cli-specs.md), [Spec documents](../guides/spec-documents.md) |
168
- | Is it a rule the project ships: a plugin, a checker, a preset? | lint | `specs/lint/<rule>/<aspect>.spec.ts` | `specification.lint()` | `lint` | [Lint specs](../guides/lint-specs.md) |
169
- | Is it a module that needs a real service, or whose oracle is a golden? | integration | `specs/integration/<domain>/<aspect>.spec.ts` | `specification.integration()` | `integration` | [Integration specs](../guides/integration-specs.md) |
170
- | Is it a module alone? | module | `__tests__/<file>.test.ts` by `<file>.ts` | none | `unit` | [Module tests](../guides/module-tests.md) |
171
- | Is it the repository itself, a suite over several apps? | repository suite | `specs/<suite>/<domain>/<aspect>.test.ts` | none | any name | [Conventions § The third door](../concepts/conventions.md#the-third-door-a-repository-suite) |
160
+ | Question | Kind | Lives at | Constructor | Project | Read |
161
+ | ---------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------ | ----------------------------- | ------------- | -------------------------------------------------------------------------------------------- |
162
+ | Does it render in a browser as a whole served page? | website | `specs/website/<domain>/<aspect>.spec.ts` | `specification.website()` | `website` | [Website specs](../guides/website-specs.md) |
163
+ | Does it render as a component (React, a DOM function, a hook)? | component | `__tests__/<file>.test.tsx` by `<file>.tsx` | none, the `component` chain | `component` | [Component specs](../guides/component-specs.md) |
164
+ | Does it render on a simulator? | mobile | `specs/mobile/<domain>/<aspect>.spec.ts` | `specification.mobile()` | `mobile` | [Mobile specs](../guides/mobile-specs.md) |
165
+ | Is it already running somewhere, met over the network (a deployed URL, a host, a port), not started by the test? | deployment | `specs/deployment/<domain>/<aspect>.spec.ts` | `specification.deployment()` | `deployment` | [Deployment specs](../guides/deployment-specs.md) |
166
+ | Does it answer HTTP? | api | `specs/api/<resource>/<action>/<aspect>.spec.ts` | `specification.api()` | `api` | [API specs](../guides/api-specs.md) |
167
+ | Is it triggered by name, in-process? | job | `specs/job/<job>/<aspect>.spec.ts` | `specification.job()` | `job` | [Job specs](../guides/job-specs.md) |
168
+ | Is it a binary? | cli | `specs/cli/<command>/` (+ `<case>.spec.yaml`) | `specification.cli(bin)` | `cli` | [CLI specs](../guides/cli-specs.md), [Spec documents](../guides/spec-documents.md) |
169
+ | Is it a rule the project ships: a plugin, a checker, a preset? | lint | `specs/lint/<rule>/<aspect>.spec.ts` | `specification.lint()` | `lint` | [Lint specs](../guides/lint-specs.md) |
170
+ | Is it a module that needs a real service, or whose oracle is a golden? | integration | `specs/integration/<domain>/<aspect>.spec.ts` | `specification.integration()` | `integration` | [Integration specs](../guides/integration-specs.md) |
171
+ | Is it a module alone? | module | `__tests__/<file>.test.ts` by `<file>.ts` | none | `unit` | [Module tests](../guides/module-tests.md) |
172
+ | Is it the repository itself, a suite over several apps? | repository suite | `specs/<suite>/<domain>/<aspect>.test.ts` | none | any name | [Conventions § The third door](../concepts/conventions.md#the-third-door-a-repository-suite) |
172
173
 
173
174
  <!-- /GENERATED:fork -->
174
175
 
175
- The order matters: a page served over HTTP is a website, not an API, because the first question already caught it. And the table is total: a folder under `specs/` is a kind's name, a special folder of what the kinds share, or a repository suite; a probe of an adapter against a real database is an integration spec like any other.
176
+ The order matters: a page served over HTTP is a website, not an API, because the first question already caught it; and an app already deployed is probed as a deployment, though it answers HTTP, because the subject is what it answers where it runs. And the table is total: a folder under `specs/` is a kind's name, a special folder of what the kinds share, or a repository suite; a probe of an adapter against a real database is an integration spec like any other.
176
177
 
177
178
  The file suffix tells the kind apart: `.test.ts` or `.test.tsx` next to the code for a single unit, `.spec.ts` under `specs/<kind>/` for an assembled product, `<case>.spec.yaml` for a CLI session written as data, and `<kind>.specification.ts` for the file that builds a runner. The kind follows the subject, not the amount of machinery: a component needs a real browser and still sits next to its code, because it is one unit; a module that needs a real database sits under `specs/integration/`, because it is assembled with that database.
178
179
 
@@ -30,7 +30,7 @@ npm install -D @jterrazz/test @jterrazz/typescript vitest
30
30
  | A lint spec, `ruleTester()` | `oxlint` |
31
31
  | Coverage | `@vitest/coverage-v8` |
32
32
 
33
- `msw`, which delivers stubbed network replies, is a direct dependency: nothing to install. An API spec hands your app to `server`, which must answer `request(path, init)` with a Fetch `Response` and, for route coverage, expose Hono's route table. A Hono app does both. Any other framework needs a wrapper of your own for the first, and an OpenAPI document named in `test: { routes }` for the second ([API specs § An app that is not Hono](../guides/api-specs.md#an-app-that-is-not-hono)).
33
+ A deployment spec needs nothing: it speaks to the network through node's own `http`, `https` and `tls`. `msw`, which delivers stubbed network replies, is a direct dependency: nothing to install. An API spec hands your app to `server`, which must answer `request(path, init)` with a Fetch `Response` and, for route coverage, expose Hono's route table. A Hono app does both. Any other framework needs a wrapper of your own for the first, and an OpenAPI document named in `test: { routes }` for the second ([API specs § An app that is not Hono](../guides/api-specs.md#an-app-that-is-not-hono)).
34
34
 
35
35
  **A missing peer is named when it is needed.** A specification's refusal names the packages it lacks and the `npm install -D` that brings them. A service's refusal names the service that asked, the peer, and the install command of the package manager your lockfile names; under pnpm a native binding must also be listed to be built, and the message adds that line:
36
36
 
@@ -100,7 +100,7 @@ It reads the `jterrazz.config.ts` beside the module whose `import.meta.url` it i
100
100
 
101
101
  ### The project helpers
102
102
 
103
- There is one helper per kind (`unit`, `integration`, `component`, `website`, `mobile`, `api`, `job`, `cli` and `lint`), named by the key of a project, and exported from `@jterrazz/test/vitest` beside `vitestConfig`, for a config written by a tool of your own; called directly, `component()` takes its `root` as an absolute path, since only `vitestConfig` knows where the project sits. Each collects its folder (`specs/<kind>/**/*.spec.ts`, plus the `<case>.spec.yaml` documents for `cli()`) and carries what its kind needs: a browser for `component()`, a simulator session for `mobile()`, and for `api()`, `job()` and `integration()` a global setup that starts each service container once and shares it across every file and project of the run ([Services § One container per run](../reference/services.md#one-container-per-run)). Docker is needed only by a spec that declares `postgres()` or `redis()`: with no Docker running, each such file fails with one sentence naming Docker, and every other file runs. Every helper accepts:
103
+ There is one helper per kind (`unit`, `integration`, `component`, `website`, `deployment`, `mobile`, `api`, `job`, `cli` and `lint`), named by the key of a project, and exported from `@jterrazz/test/vitest` beside `vitestConfig`, for a config written by a tool of your own; called directly, `component()` takes its `root` as an absolute path, since only `vitestConfig` knows where the project sits. Each collects its folder (`specs/<kind>/**/*.spec.ts`, plus the `<case>.spec.yaml` documents for `cli()`) and carries what its kind needs: a browser for `component()`, a simulator session for `mobile()`, and for `api()`, `job()` and `integration()` a global setup that starts each service container once and shares it across every file and project of the run ([Services § One container per run](../reference/services.md#one-container-per-run)). Docker is needed only by a spec that declares `postgres()` or `redis()`: with no Docker running, each such file fails with one sentence naming Docker, and every other file runs. Every helper accepts:
104
104
 
105
105
  | Option | Does |
106
106
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "18.1.0",
3
+ "version": "18.2.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, background jobs, websites, mobile apps, rendered components and the rules a project ships: one entry point, real infrastructure and a real browser, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",