@jterrazz/test 16.1.1 → 17.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.
Files changed (304) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +2 -2980
  3. package/dist/browser/services.js +2 -0
  4. package/dist/browser/stubs.js +2 -0
  5. package/dist/checker.d.ts +6 -1
  6. package/dist/checker.js +4534 -2628
  7. package/dist/config.d.ts +40 -0
  8. package/dist/config.js +309 -0
  9. package/dist/in-place.global-setup.d.ts +11 -0
  10. package/dist/in-place.global-setup.js +21 -0
  11. package/dist/index.d.ts +1653 -3004
  12. package/dist/index.js +5226 -3845
  13. package/dist/mobile-session.global-setup.d.ts +14 -0
  14. package/dist/mobile-session.global-setup.js +31 -0
  15. package/dist/oxlint.cjs +6069 -5634
  16. package/dist/oxlint.d.cts +43 -7212
  17. package/dist/oxlint.d.ts +43 -7212
  18. package/dist/oxlint.js +6127 -5693
  19. package/dist/route-coverage.global-setup.d.ts +42 -0
  20. package/dist/route-coverage.global-setup.js +2 -0
  21. package/dist/services.d.ts +3 -0
  22. package/dist/services.global-setup.d.ts +43 -0
  23. package/dist/services.global-setup.js +2 -0
  24. package/dist/services.js +597 -0
  25. package/dist/shared-appium.adapter.js +428 -0
  26. package/dist/shared-artifacts.js +32 -0
  27. package/dist/shared-chapter.js +7 -0
  28. package/dist/shared-container.port.d.ts +31 -0
  29. package/dist/shared-derived-projects.js +58 -0
  30. package/dist/shared-device.port.d.ts +189 -0
  31. package/dist/shared-exchange.js +447 -0
  32. package/dist/shared-in-place.js +172 -0
  33. package/dist/shared-index.browser.js +3191 -0
  34. package/dist/shared-index.services.d.ts +346 -0
  35. package/dist/shared-index.stubs.d.ts +287 -0
  36. package/dist/shared-json-shape.js +17 -0
  37. package/dist/shared-landmarks.js +71 -0
  38. package/dist/shared-ledger.js +68 -0
  39. package/dist/shared-literate-plugin.js +183 -0
  40. package/dist/shared-manifest.js +1321 -0
  41. package/dist/shared-pause.js +11 -0
  42. package/dist/shared-peer.js +88 -0
  43. package/dist/shared-playwright.adapter.js +330 -0
  44. package/dist/shared-process.adapter.js +74 -0
  45. package/dist/shared-refusal.js +35 -0
  46. package/dist/shared-resolve.js +24 -0
  47. package/dist/shared-route-coverage.global-setup.js +187 -0
  48. package/dist/shared-scenario-failure.js +198 -0
  49. package/dist/shared-serve.js +365 -0
  50. package/dist/shared-service-images.js +10 -0
  51. package/dist/shared-services.global-setup.js +179 -0
  52. package/dist/shared-session-ledger.js +301 -0
  53. package/dist/shared-setup-entry.js +12 -0
  54. package/dist/shared-source-scan.js +133 -0
  55. package/dist/shared-spec-document.js +365 -0
  56. package/dist/shared-specifications.js +26 -0
  57. package/dist/shared-stream-body.js +130 -0
  58. package/dist/shared-stub-server.js +190 -0
  59. package/dist/shared-temp-root.global-setup.js +80 -0
  60. package/dist/shared-test-baseline.js +26 -0
  61. package/dist/shared-test-section.js +58 -0
  62. package/dist/shared-testcontainers.adapter.js +203 -0
  63. package/dist/shared-tokens.js +55 -0
  64. package/dist/shared-yaml-document.js +119 -0
  65. package/dist/stubs.d.ts +2 -0
  66. package/dist/stubs.js +657 -0
  67. package/dist/temp-root.global-setup.d.ts +7 -0
  68. package/dist/temp-root.global-setup.js +2 -0
  69. package/dist/vitest.d.ts +97 -163
  70. package/dist/vitest.js +373 -525
  71. package/docs/README.md +82 -36
  72. package/docs/concepts/architecture.md +118 -0
  73. package/docs/concepts/conventions.md +333 -0
  74. package/docs/guides/api-specs.md +374 -0
  75. package/docs/guides/cli-specs.md +564 -0
  76. package/docs/guides/component-specs.md +252 -0
  77. package/docs/guides/develop.md +94 -0
  78. package/docs/guides/integration-specs.md +181 -0
  79. package/docs/guides/job-specs.md +180 -0
  80. package/docs/guides/lint-specs.md +174 -0
  81. package/docs/guides/mobile-specs.md +319 -0
  82. package/docs/guides/module-tests.md +149 -0
  83. package/docs/guides/release.md +43 -0
  84. package/docs/guides/spec-documents.md +189 -0
  85. package/docs/guides/website-specs.md +413 -0
  86. package/docs/reference/assertions.md +345 -0
  87. package/docs/reference/glossary.md +20 -0
  88. package/docs/reference/linting.md +483 -0
  89. package/docs/reference/services.md +236 -0
  90. package/docs/reference/stubs.md +498 -0
  91. package/docs/reference/time-and-doubles.md +137 -0
  92. package/docs/reference/tokens.md +258 -0
  93. package/docs/reference/visitor.md +221 -0
  94. package/docs/start/getting-started.md +187 -0
  95. package/docs/start/set-up-a-project.md +214 -0
  96. package/package.json +50 -30
  97. package/schema/spec.schema.json +43 -17
  98. package/dist/appium.adapter.js +0 -489
  99. package/dist/artifacts.js +0 -32
  100. package/dist/fixtures.js +0 -110
  101. package/dist/literate.js +0 -1747
  102. package/dist/match.js +0 -804
  103. package/dist/playwright.adapter.js +0 -438
  104. package/dist/server.js +0 -160
  105. package/dist/stream.js +0 -321
  106. package/dist/substring-warning.js +0 -282
  107. package/docs/01-architecture.md +0 -95
  108. package/docs/02-developing.md +0 -351
  109. package/docs/03-testing.md +0 -495
  110. package/docs/04-operating.md +0 -101
  111. package/docs/05-module-tests.md +0 -152
  112. package/docs/06-integration.md +0 -180
  113. package/docs/07-component.md +0 -246
  114. package/docs/08-website.md +0 -404
  115. package/docs/09-mobile.md +0 -270
  116. package/docs/10-api.md +0 -264
  117. package/docs/11-jobs.md +0 -157
  118. package/docs/12-cli.md +0 -677
  119. package/docs/13-elements.md +0 -248
  120. package/docs/14-assertions.md +0 -314
  121. package/docs/15-tokens.md +0 -253
  122. package/docs/16-contracts.md +0 -438
  123. package/docs/17-services.md +0 -188
  124. package/docs/18-conventions.md +0 -332
  125. package/docs/19-linting.md +0 -512
  126. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  127. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  128. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  129. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  130. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  131. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  132. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  133. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  134. package/docs/decisions/_template.md +0 -22
  135. package/docs/reference/classes/Matcher.md +0 -120
  136. package/docs/reference/classes/ProcessHandle.md +0 -368
  137. package/docs/reference/functions/defineContract.md +0 -39
  138. package/docs/reference/functions/defineContracts.md +0 -36
  139. package/docs/reference/functions/disabled.md +0 -28
  140. package/docs/reference/functions/enabled.md +0 -20
  141. package/docs/reference/functions/focused.md +0 -26
  142. package/docs/reference/functions/mockOf.md +0 -79
  143. package/docs/reference/functions/postgres.md +0 -31
  144. package/docs/reference/functions/process.md +0 -33
  145. package/docs/reference/functions/redis.md +0 -28
  146. package/docs/reference/functions/required.md +0 -41
  147. package/docs/reference/functions/selected.md +0 -26
  148. package/docs/reference/functions/sqlite.md +0 -33
  149. package/docs/reference/functions/testId.md +0 -20
  150. package/docs/reference/functions/text.md +0 -40
  151. package/docs/reference/functions/valued.md +0 -28
  152. package/docs/reference/functions/waitUntil.md +0 -46
  153. package/docs/reference/functions/within.md +0 -29
  154. package/docs/reference/index.md +0 -196
  155. package/docs/reference/interfaces/BaseResult.md +0 -87
  156. package/docs/reference/interfaces/CallResult.md +0 -160
  157. package/docs/reference/interfaces/CaptureScope.md +0 -82
  158. package/docs/reference/interfaces/CliResult.md +0 -253
  159. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  160. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  161. package/docs/reference/interfaces/FetchResult.md +0 -180
  162. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  163. package/docs/reference/interfaces/HttpResult.md +0 -126
  164. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  165. package/docs/reference/interfaces/PageResult.md +0 -356
  166. package/docs/reference/interfaces/RenderResult.md +0 -72
  167. package/docs/reference/interfaces/RenderedText.md +0 -110
  168. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  169. package/docs/reference/interfaces/ScreenResult.md +0 -128
  170. package/docs/reference/interfaces/TableAccessor.md +0 -26
  171. package/docs/reference/interfaces/TextAccessor.md +0 -96
  172. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  173. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  174. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  175. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  176. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  177. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  178. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  179. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  180. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  181. package/docs/reference/type-aliases/CliEnv.md +0 -11
  182. package/docs/reference/type-aliases/CliHandle.md +0 -80
  183. package/docs/reference/type-aliases/CliInput.md +0 -36
  184. package/docs/reference/type-aliases/CliOutput.md +0 -46
  185. package/docs/reference/type-aliases/CliPort.md +0 -58
  186. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  187. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  188. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  189. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  190. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  191. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  192. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  193. package/docs/reference/type-aliases/Contract.md +0 -68
  194. package/docs/reference/type-aliases/ContractInput.md +0 -13
  195. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  196. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  197. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  198. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  199. package/docs/reference/type-aliases/Contracts.md +0 -48
  200. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  201. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  202. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  203. package/docs/reference/type-aliases/DevicePort.md +0 -51
  204. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  205. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  206. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  207. package/docs/reference/type-aliases/DomMount.md +0 -21
  208. package/docs/reference/type-aliases/ElementKind.md +0 -27
  209. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  210. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  211. package/docs/reference/type-aliases/ElementRef.md +0 -128
  212. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  213. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  214. package/docs/reference/type-aliases/HonoApp.md +0 -31
  215. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  216. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  217. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  218. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  219. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  220. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  221. package/docs/reference/type-aliases/Intercept.md +0 -19
  222. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  223. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  224. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  225. package/docs/reference/type-aliases/JobHandle.md +0 -34
  226. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  227. package/docs/reference/type-aliases/JobsResult.md +0 -16
  228. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  229. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  230. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  231. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  232. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  233. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  234. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  235. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  236. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  237. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  238. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  239. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  240. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  241. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  242. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  243. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  244. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  245. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  246. package/docs/reference/type-aliases/MockPort.md +0 -10
  247. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  248. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  249. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  250. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  251. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  252. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  253. package/docs/reference/type-aliases/ServerPort.md +0 -36
  254. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  255. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  256. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  257. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  258. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  259. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  260. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  261. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  262. package/docs/reference/type-aliases/SpecKind.md +0 -8
  263. package/docs/reference/type-aliases/SpecRun.md +0 -108
  264. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  265. package/docs/reference/type-aliases/SpecStream.md +0 -32
  266. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  267. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  268. package/docs/reference/type-aliases/SseEvent.md +0 -58
  269. package/docs/reference/type-aliases/StreamBody.md +0 -61
  270. package/docs/reference/type-aliases/TextFilter.md +0 -22
  271. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  272. package/docs/reference/type-aliases/Visitor.md +0 -215
  273. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  274. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  275. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  276. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  277. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  278. package/docs/reference/variables/anthropic.md +0 -19
  279. package/docs/reference/variables/banner.md +0 -10
  280. package/docs/reference/variables/button.md +0 -10
  281. package/docs/reference/variables/clock.md +0 -16
  282. package/docs/reference/variables/complementary.md +0 -10
  283. package/docs/reference/variables/component.md +0 -10
  284. package/docs/reference/variables/content.md +0 -10
  285. package/docs/reference/variables/contentinfo.md +0 -10
  286. package/docs/reference/variables/dialog.md +0 -10
  287. package/docs/reference/variables/field.md +0 -10
  288. package/docs/reference/variables/form.md +0 -10
  289. package/docs/reference/variables/heading.md +0 -10
  290. package/docs/reference/variables/http.md +0 -40
  291. package/docs/reference/variables/intercept.md +0 -11
  292. package/docs/reference/variables/link.md +0 -10
  293. package/docs/reference/variables/listitem.md +0 -10
  294. package/docs/reference/variables/main.md +0 -10
  295. package/docs/reference/variables/match.md +0 -8
  296. package/docs/reference/variables/navigation.md +0 -10
  297. package/docs/reference/variables/openai.md +0 -21
  298. package/docs/reference/variables/option.md +0 -17
  299. package/docs/reference/variables/region.md +0 -10
  300. package/docs/reference/variables/row.md +0 -10
  301. package/docs/reference/variables/search.md +0 -10
  302. package/docs/reference/variables/specification.md +0 -8
  303. package/docs/reference/variables/status.md +0 -10
  304. package/docs/reference/variables/table.md +0 -10
@@ -0,0 +1,180 @@
1
+ # Job specs (`specification.job`)
2
+
3
+ `specification.job()` tests background pipelines (cron jobs, queue consumers, nightly reports) by triggering them in-process against real databases and stubbed external providers. No HTTP server is involved: the subject under test is _what a job writes_, not what an endpoint returns.
4
+
5
+ Use it when the behaviour you care about starts with "when the job runs...". If the behaviour starts with an HTTP request, use [api](api-specs.md).
6
+
7
+ ## What it specifies
8
+
9
+ A job spec answers one question: **when this name is triggered, what does the pipeline do to the world it was handed?** The subject is the assembled job (its handler, its services, the network it reaches), met through the one door the scheduler would use, `.trigger(name)`. What a single pure function inside that pipeline returns is a module test's ([Module tests](module-tests.md)); what the pipeline leaves in a database is this page's.
10
+
11
+ The scheduler that invokes the job in production (a cron, a queue) is out of scope: the job is the subject, and its name is how a spec reaches it.
12
+
13
+ ## The constructor
14
+
15
+ ```typescript
16
+ // specs/job/job.specification.ts
17
+ import { afterAll } from 'vitest';
18
+ import { specification } from '@jterrazz/test';
19
+ import { postgres } from '@jterrazz/test/services';
20
+ import { nightlyReport, supportDrafts } from '../../src/jobs.js';
21
+
22
+ // Hoisted, so a test can read this database after a chain that rejected
23
+ export const analyticsDb = postgres(); // → "analytics-db", init from docker/analytics-db/
24
+
25
+ export const { job, cleanup } = await specification.job({
26
+ services: { db: postgres(), analyticsDb },
27
+ jobs: ({ db, analyticsDb }) => [nightlyReport(db, analyticsDb), supportDrafts(db)],
28
+ });
29
+
30
+ afterAll(cleanup);
31
+ ```
32
+
33
+ ### Options
34
+
35
+ | Option | Required | Description |
36
+ | ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
37
+ | `services` | yes (if the jobs need infra) | Named record of service factories, same semantics as [api](api-specs.md), see [services](../reference/services.md) |
38
+ | `jobs` | yes | `(services) => Job[]`, or a static array. Each is a named, triggerable job (rule `server-sees-declared-services`) |
39
+ | `root` | no | Root-resolution override, same walk-up rule as everywhere (rule `no-redundant-root`) |
40
+
41
+ There is **no `server`** option: jobs run in-process by definition, and the record of services is the only outside world they meet. A `Job` carries the name you pass to `.trigger()`: the handles above respond to `'nightly-report'` and `'support-drafts'`.
42
+
43
+ ## The chain
44
+
45
+ Setups: `.seed(file('./__seeds__/<name>.sql'))`, `.intercept()`, `.clock()`, and `job.with({ clock })`, a view whose every chain starts at the instant the whole file shares ([Conventions](../concepts/conventions.md#what-a-file-shares)). Terminal action: `.trigger(name)` (rule `one-terminal-action-per-chain`). No `.headers()`: there is no request.
46
+
47
+ `.clock('2026-03-04T09:30:00Z')` pins the job's own `Date` for the chain and releases it when the action resolves: a nightly report that stamps a window, an expiry the job computes. The primitive and its second depth are [Time and doubles § The clock](../reference/time-and-doubles.md#the-clock).
48
+
49
+ ```typescript
50
+ // specs/job/nightly-report/pipeline.spec.ts: the job, and the aspect this file asks of it
51
+ import { file } from '@jterrazz/test';
52
+ import { describe, expect, test } from 'vitest';
53
+ import { job } from '../job.specification.js';
54
+ import pipeline from './__setup__/stubs/pipeline.stubs.js';
55
+
56
+ describe('pipeline', () => {
57
+ test('nightly report classifies, prices and drafts', async () => {
58
+ // Given - one pending article + the world the pipeline reaches out to
59
+ const result = await job
60
+ .seed(file('./__seeds__/one-pending-article.sql'), { database: 'db' })
61
+ .intercept(pipeline)
62
+ .trigger('nightly-report');
63
+
64
+ // Then - the pipeline produced a classified, priced report
65
+ await expect(result.table('reports', { database: 'analyticsDb' })).toMatch([
66
+ [1, 'ELECTRONICS', 109],
67
+ ]);
68
+ });
69
+ });
70
+ ```
71
+
72
+ Everything a pipeline reads from the outside world is declared: seeds set the database state, stubs pin the external providers (OpenAI, Anthropic, arbitrary HTTP; see [stubs](../reference/stubs.md)). Databases reset at the start of every chain, exactly as for API specs (rules `one-terminal-action-per-chain`, `sequence-expressed-by-seeding`).
73
+
74
+ Because jobs run in-process by definition, `.intercept()` is always available. It is **strict** (rule `network-strict-after-first-stub`): once a chain declares one stub, an outgoing request no stub answers fails the spec ([Stubs § Strict by construction](../reference/stubs.md#strict-by-construction)). A chain with no stubs is not network-guarded.
75
+
76
+ ### Seeding and sequences for pipelines
77
+
78
+ Selection is **first non-exhausted match wins**, so a multi-call scenario is a finite stub in front of an unlimited tail; `times` says how many calls the first one answers:
79
+
80
+ ```typescript
81
+ test('retries then recovers from provider rate-limit', async () => {
82
+ // Given - the first call is rate-limited, everything after it succeeds
83
+ const result = await job
84
+ .seed(file('./__seeds__/pending-articles.sql'), { database: 'db' })
85
+ .intercept([
86
+ { request: openai.chat(), response: openai.error(429), times: 1 },
87
+ { request: openai.chat(), response: openai.reply({ category: 'BOOKS' }) },
88
+ ])
89
+ .trigger('nightly-report');
90
+
91
+ // Then - the retry succeeded
92
+ await expect(result.table('reports', { database: 'analyticsDb' })).toMatch([
93
+ [1, 'BOOKS', '{{number}}'],
94
+ ]);
95
+ });
96
+ ```
97
+
98
+ The number in `times` IS the assertion: it says the job retries exactly once. A stub with no `times` is unlimited, which is what you want for a route the pipeline may hit any number of times.
99
+
100
+ ## The result
101
+
102
+ `.trigger()` resolves to a result whose primary subjects are the databases:
103
+
104
+ | Member | Description |
105
+ | --------------------------- | --------------------------------------------------------------------------- |
106
+ | `result.table(name, opts?)` | Table subject, compared whole: `toMatch(<rows>)` / `toBeEmpty()`, `await`ed |
107
+
108
+ Whether `.seed()` and `.table()` name a database is [Services § The `database:` rule](../reference/services.md#the-database-rule).
109
+
110
+ ## Unique here
111
+
112
+ ### Error-case testing: a provider that fails on purpose
113
+
114
+ Provider failure modes are first-class response builders. This is where job specs earn their keep, because these paths are nearly impossible to reproduce against live providers:
115
+
116
+ ```typescript
117
+ test('handles malformed model output', async () => {
118
+ // Given - the model returns something that is not JSON
119
+ const result = await job
120
+ .intercept(openai.chat(), openai.malformed('not json at all'))
121
+ .trigger('nightly-report');
122
+
123
+ // Then - nothing half-written
124
+ await expect(result.table('reports', { database: 'analyticsDb' })).toBeEmpty();
125
+ });
126
+
127
+ test('times out gracefully', async () => {
128
+ // Given - a silent provider (delay longer than the job timeout)
129
+ const result = await job
130
+ .intercept(anthropic.messages(), anthropic.timeout())
131
+ .trigger('support-drafts');
132
+
133
+ // Then
134
+ await expect(result.table('support_drafts', { database: 'db' })).toBeEmpty();
135
+ });
136
+ ```
137
+
138
+ The three failure families:
139
+
140
+ | Builder | Simulates |
141
+ | ---------------------------------------------------------------------- | ------------------------------------------------------ |
142
+ | `openai.error(429)` / `anthropic.error(status)` / `http.error(status)` | HTTP error status from the provider |
143
+ | `openai.timeout()` / `anthropic.timeout()` | A provider that never answers within the job's timeout |
144
+ | `openai.malformed('...')` | A 200 whose body violates the provider schema |
145
+
146
+ The full builder catalogue lives in [stubs](../reference/stubs.md).
147
+
148
+ ### A crash is read by its rejection
149
+
150
+ A job that throws rejects `.trigger()` with its own error, and what it wrote before the throw stays in the database. Read it from the service handle, since a rejected chain hands back no result. That is why the constructor above hoists `analyticsDb` out of the record: a handle declared on its own is one a test can import and query.
151
+
152
+ ```typescript
153
+ import { describe, expect, test } from 'vitest';
154
+
155
+ import { analyticsDb, job } from '../job.specification.js';
156
+
157
+ describe('crash', () => {
158
+ test('a crashed job leaves what it wrote first', async () => {
159
+ // Given - a job that writes one row, then throws
160
+ const run = job.trigger('crash-after-insert');
161
+
162
+ // Then - its error, and the row it wrote before it
163
+ await expect(run).rejects.toThrow('crash-after-insert: boom');
164
+ await expect(analyticsDb.query('events', ['type'])).resolves.toStrictEqual([['pre_crash']]);
165
+ });
166
+ });
167
+ ```
168
+
169
+ ## Pitfalls
170
+
171
+ - **Looking for a `server` option.** It exists only on `specification.api()` and `specification.website()`; jobs are in-process by definition; what a job reads from the outside is its `services` record (rule `server-sees-declared-services`).
172
+ - **Asserting on a job's return value.** The result surface is the observable state (tables); jobs are specified by their effects.
173
+ - **Forgetting `times` on the leading stub of a sequence.** With no `times` the first stub is unlimited, so it answers every call and the recovery stub behind it is never reached.
174
+ - **One giant spec that triggers two jobs.** A chain has exactly one terminal action (rule `one-terminal-action-per-chain`); sequence scenarios are expressed by seeding the state the second job would have found (rule `sequence-expressed-by-seeding`).
175
+ - **Forgetting `await` on table expectations.** `expect(result.table(...))` without `await` never runs the SQL: table matchers are IO matchers, typed `Promise<void>`, and the type-aware `typescript/no-floating-promises` refuses one dropped.
176
+ - **Bounding the tail of a sequence.** The last stub of a sequence should stay unlimited; a `times` there turns one extra retry into an "unmatched outgoing request" failure (rule `network-strict-after-first-stub`).
177
+
178
+ ## Related
179
+
180
+ [API specs](api-specs.md) · [Assertions](../reference/assertions.md) · [Stubs](../reference/stubs.md) · [Services](../reference/services.md)
@@ -0,0 +1,174 @@
1
+ # Lint specs (`specification.lint`)
2
+
3
+ `specification.lint()` tests the rules a project ships (an oxlint plugin, a cross-file checker, the laws a `@jterrazz/typescript` preset holds a tree to) by running the real linters over fixture trees and comparing their whole report. The subject is a rule, met the way a consumer meets it: a tree in, the linter's verdict out.
4
+
5
+ Use it when the behaviour you care about starts with "when this tree is linted...". A rule's visitor alone, on one snippet, is a module test with [`ruleTester()`](#a-rule-alone-ruletester); the tree the rule is meant to judge, with its folders and its neighbours, is this page's.
6
+
7
+ ## What it specifies
8
+
9
+ A lint spec answers one question: **when this tree is linted, where does which rule report, and what does it say?** A rule is a real subject a team tests, like a binary: it has inputs (a tree), a contract (a report), and consumers who read that report. A rule reading ONE file is proven by its report over a tree holding that file; a rule reading the whole tree (a folder's shape, a manifest, what imports what) can only be proven that way, so one specification serves both.
10
+
11
+ What it does not do: judge the tree it runs in. The specification lints a fixture laid in a fresh working directory, never the repository's own sources; those are `typescript check`'s.
12
+
13
+ ## The constructor
14
+
15
+ The examples below are a package that ships an oxlint plugin and a preset, both built to its own `dist/`: `dist/oxlint.js` is that package's built plugin, and `dist/config.js` exports its preset, here called `acme()`. Use the paths your build writes, or the published specifier of your package (`'@acme/lint/oxlint'`). A plugin is loaded from what was built, so build before the lint project runs ([§ Pitfalls](#pitfalls)).
16
+
17
+ ```typescript
18
+ // specs/lint/lint.specification.ts
19
+ import { afterAll } from 'vitest';
20
+
21
+ import { recommendedRules } from '../../dist/oxlint.js';
22
+ import { specification } from '@jterrazz/test';
23
+
24
+ export const { lint, cleanup } = await specification.lint({
25
+ config: { categories: { correctness: 'off' }, rules: recommendedRules },
26
+ plugins: ['../../dist/oxlint.js'],
27
+ });
28
+
29
+ afterAll(cleanup);
30
+ ```
31
+
32
+ A preset's laws, or a checker of the project's own, are linters of the same specification:
33
+
34
+ ```typescript
35
+ // specs/lint/preset-lint.specification.ts
36
+ import { afterAll } from 'vitest';
37
+
38
+ import { acme } from '../../dist/config.js';
39
+ import { specification } from '@jterrazz/test';
40
+
41
+ export const { lint, cleanup } = await specification.lint({ presets: [acme()] });
42
+
43
+ afterAll(cleanup);
44
+ ```
45
+
46
+ ### Options
47
+
48
+ At least one linter: a plugin, a config, a checker or presets, `presets: []` holding the tree to the base preset alone; a specification naming none is refused.
49
+
50
+ | Option | Description |
51
+ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `plugins` | oxlint JS plugins, each a path from the specification's folder or a package specifier. They run in the real oxlint, `oxlint` being an optional peer the project installs |
53
+ | `config` | The oxlint config oxlint runs under (`rules`, `settings`, `categories`, `overrides`), less `jsPlugins`, which `plugins` states; named alone, it runs oxlint's built-in rules |
54
+ | `presets` | `@jterrazz/typescript` presets whose laws run on the tree (the Architecture pass over every `layers`, each declared `names` rule), resolved after the always-on base preset as `jterrazz.config.ts` would; `[]` is the base preset alone |
55
+ | `checkers` | Cross-file checkers, each a command printing one finding a line in the compiler's convention, `<file>:<line>:<column>: error <plugin>(<rule>): <message>`; `.run()` hands it the tree last |
56
+ | `root` | Root-resolution override (rule `no-redundant-root`); the plugins' specifiers and the checkers' commands resolve from its `node_modules` |
57
+ | `settings` | What the project adds beside its presets, written as `jterrazz.config.ts` writes it (`typescript: { names, architecture: { layers } }`, `optOuts`), so a repository's own additions are its own, never a preset it makes up |
58
+
59
+ There is nothing to start and nothing to stop: `cleanup` is handed to `afterAll` like every constructor's, and releases nothing.
60
+
61
+ ## The chain
62
+
63
+ Setups: `.fixture(directory('./__fixtures__/<tree>/'))` (or a `file()`, copied by name) lays the tree in a fresh working directory ([CLI specs § Fixtures](cli-specs.md#the-chain)); `.rules({ ... })` states rules and their options for this chain, over the specification's `config`, a later call winning; on a specification that runs no oxlint (no plugin, no config) it is refused, since no linter would read it. Terminal action: `.run()` (rule `one-terminal-action-per-chain`).
64
+
65
+ ```typescript
66
+ // specs/lint/no-module-mock/no-module-mock.spec.ts
67
+ import { describe, expect, test } from 'vitest';
68
+ import { directory } from '@jterrazz/test';
69
+
70
+ import { lint } from '../lint.specification.js';
71
+
72
+ describe('no-module-mock', () => {
73
+ test('rejects an allow-list entry with no reason', async () => {
74
+ // Given - a module on the allow-list, and no reason beside it
75
+ const result = await lint
76
+ .rules({ 'acme/no-module-mock': ['error', { modules: [{ module: 'x', reason: '' }] }] })
77
+ .fixture(directory('./__fixtures__/allow-listed/'))
78
+ .run();
79
+
80
+ // Then - the finding, where it fires, with its lesson
81
+ expect(result.report).toMatch(`
82
+ src/storage.test.ts:5:1 acme(no-module-mock)
83
+ found: "x" is on the \`modules\` allow-list with no reason
84
+ why: A test doubles a PORT, never a module.
85
+ fix: state why no consumer can inject it
86
+ read: docs/reference/linting.md#no-module-mock
87
+ `);
88
+ });
89
+ });
90
+ ```
91
+
92
+ `.run()` takes two options:
93
+
94
+ | Option | Description |
95
+ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96
+ | `fix` | Apply oxlint's and the checkers' fixes, then lint the fixed tree: the report is what a run after the fix says, and `result.file()` reads what it wrote; the presets' laws have no fix, and are judged on the fixed tree |
97
+ | `args` | Handed to every checker before the tree, as a checker takes a flag (`--member packages/web`) |
98
+
99
+ ## The result
100
+
101
+ | Member | Description |
102
+ | ------------------------ | ------------------------------------------------------------------- |
103
+ | `result.report` | Every linter's diagnostics, as a text subject (see below) |
104
+ | `result.file(path)` | A file of the tree after the run, relative to the working directory |
105
+ | `result.directory(path)` | A folder of the tree, compared whole to a golden tree |
106
+
107
+ ### The report
108
+
109
+ One diagnostic is its head line, `<file>:<line>:<column> <plugin>(<rule>)`, then its message: a lesson a part per line, `found:`, `why:`, `fix:`, `read:`, or the message as the linter wrote it:
110
+
111
+ ```text
112
+ specs/app/widget/widget.test.ts:8:36 acme(no-sleep)
113
+ found: an arbitrary sleep in a test
114
+ why: No arbitrary sleep in any test file.
115
+ fix: wait on what the sleep stands for
116
+ read: docs/reference/linting.md#no-sleep
117
+ ```
118
+
119
+ The diagnostics come in the order a reader walks the tree (file, then line, then column), whichever linter printed them and whatever order its threads finished in; a tally, a summary or any other line is not one. A clean tree is the empty text: `expect(result.report).toMatch('')`. Written in place and rewritten by `TEST_UPDATE=1` ([Tokens](../reference/tokens.md#update-mode-tokens-are-preserved)), the golden proves where each rule fired, which one, and what it teaches.
120
+
121
+ oxlint's graphical rendering is refused, never read as a clean run: the specification states `--format=unix` itself, and a report handed to it in frames names the format to state.
122
+
123
+ ## Unique here
124
+
125
+ ### What `.run()` runs
126
+
127
+ Each linter the specification names, over the working directory:
128
+
129
+ 1. **oxlint**, when `plugins` or `config` is named: the real binary, with a config the specification writes outside the tree (`jsPlugins` the plugins, `rules` the specification's `config.rules` and the chain's `.rules()` over them), at `--format=unix`.
130
+ 2. **each checker**, as `<command> <args> .`, from the tree, its first word resolved from the project's `node_modules/.bin` when it is installed there.
131
+ 3. **the presets' laws**, in process, through `presetChecks` of `@jterrazz/typescript/rules`, handed the `presets` and the `settings` as one settings object: each finding at its position, one about a whole file or folder at `:1:1`. Settings the kit cannot resolve are refused like a linter that fails with no diagnostic.
132
+
133
+ A preset's own `oxlint` (its plugin, its rulebook) is not run by `presets`: the presets hand the specification their laws alone. A preset's rules are proven as any plugin's are, the plugin under `plugins` and its rules under `config`, so a report holds only the rules the specification names.
134
+
135
+ A linter that fails and reports no diagnostic is refused in four parts rather than read as a clean tree: a plugin that does not load, a config oxlint cannot build, a checker that cannot start.
136
+
137
+ ### A rule pack: a nested Vitest and a reporter fixture
138
+
139
+ A package that ships lint rules for other projects proves them by running the real binary on a tree: the spec starts a nested `vitest` over a fixture project (`.fixture(directory(...))`) with a reporter fixture that prints one line per finding, and compares that output whole. A rule alone is `ruleTester()`.
140
+
141
+ ### A rule alone: `ruleTester()`
142
+
143
+ A rule's visitor, on snippets, is a module test in the `__tests__/` of the rule's folder. `ruleTester()` is oxlint's `RuleTester` (`oxlint/plugins-dev`) bound to vitest (its suites and cases are vitest's), and it requires a position on every invalid case: each expected error is an object stating its `line` and `column`. A count, an empty list, or an error with no position, is refused before the case runs, since it passes for a report on any node.
144
+
145
+ The two tools count columns differently, and each keeps its own convention: `ruleTester()` reads oxlint's RuleTester columns, **0-based** (`column: 25` below is the `setTimeout` at index 25), while a lint spec's report prints the compiler's `file:line:column`, **1-based**, so the same finding reads `:1:26` there.
146
+
147
+ ```typescript
148
+ // src/rules/no-sleep.test.ts
149
+ import { ruleTester } from '@jterrazz/test';
150
+
151
+ import { noSleep } from './no-sleep.js';
152
+
153
+ ruleTester().run('no-sleep', noSleep, {
154
+ invalid: [
155
+ {
156
+ code: 'await new Promise((r) => setTimeout(r, 100));',
157
+ errors: [{ column: 25, line: 1, messageId: 'slept' }],
158
+ filename: '/project/specs/api/users/list.spec.ts',
159
+ },
160
+ ],
161
+ valid: ['await waitUntil(() => ready);'],
162
+ });
163
+ ```
164
+
165
+ ## Pitfalls
166
+
167
+ - **Linting a stale build.** The plugins and checkers a project ships are usually loaded from its `dist/`: build before the lint project runs, or the specs judge the previous rules.
168
+ - **A golden cut to the rule ids.** The report carries each lesson whole, so a reworded message moves the golden: that is the contract a reader meets, and `TEST_UPDATE=1` rewrites it.
169
+ - **A literal token in a message.** A lesson that echoes `{{uuid}}` is read as the token in the golden; cover the echo with `{{string}}` ([Tokens § Known limitations](../reference/tokens.md#known-limitations)).
170
+ - **One specification for every linter.** A plugin's fixture judged by the checker too reports what it was never built to show: give each linter a specification of its own (`checker-lint.specification.ts`, `preset-lint.specification.ts`).
171
+
172
+ ## Related
173
+
174
+ [CLI specs](cli-specs.md) · [Assertions](../reference/assertions.md) · [Tokens](../reference/tokens.md) · [Linting](../reference/linting.md)