@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.
Files changed (309) hide show
  1. package/README.md +45 -171
  2. package/dist/browser/index.js +1 -2980
  3. package/dist/browser/services.js +1 -0
  4. package/dist/browser/stubs.js +1 -0
  5. package/dist/browser-shared-index.browser.js +3233 -0
  6. package/dist/checker.d.ts +6 -1
  7. package/dist/checker.js +3721 -2624
  8. package/dist/config.d.ts +40 -0
  9. package/dist/config.js +309 -0
  10. package/dist/in-place.global-setup.d.ts +12 -0
  11. package/dist/in-place.global-setup.js +21 -0
  12. package/dist/index.d.ts +1641 -3004
  13. package/dist/index.js +5095 -3835
  14. package/dist/mobile-session.global-setup.d.ts +15 -0
  15. package/dist/mobile-session.global-setup.js +31 -0
  16. package/dist/oxlint.cjs +6069 -5634
  17. package/dist/oxlint.d.cts +43 -7212
  18. package/dist/oxlint.d.ts +43 -7212
  19. package/dist/oxlint.js +3979 -5799
  20. package/dist/route-coverage.global-setup.d.ts +43 -0
  21. package/dist/route-coverage.global-setup.js +1 -0
  22. package/dist/services.d.ts +3 -0
  23. package/dist/services.global-setup.d.ts +43 -0
  24. package/dist/services.global-setup.js +1 -0
  25. package/dist/services.js +597 -0
  26. package/dist/shared-appium.adapter.js +428 -0
  27. package/dist/shared-artifacts.js +34 -0
  28. package/dist/shared-chapter.js +7 -0
  29. package/dist/shared-container.port.d.ts +31 -0
  30. package/dist/shared-derived-projects.js +73 -0
  31. package/dist/shared-device.port.d.ts +189 -0
  32. package/dist/shared-exchange.js +447 -0
  33. package/dist/shared-in-place.js +172 -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-listened-port.js +19 -0
  40. package/dist/shared-literate-plugin.js +184 -0
  41. package/dist/shared-manifest.js +1321 -0
  42. package/dist/shared-pause.js +22 -0
  43. package/dist/shared-peer.js +88 -0
  44. package/dist/shared-playwright.adapter.js +328 -0
  45. package/dist/shared-process-alive.js +13 -0
  46. package/dist/shared-process.adapter.js +74 -0
  47. package/dist/shared-record-key.js +13 -0
  48. package/dist/shared-refusal.js +35 -0
  49. package/dist/shared-resolve.js +24 -0
  50. package/dist/shared-route-coverage.global-setup.js +187 -0
  51. package/dist/shared-scenario-failure.js +198 -0
  52. package/dist/shared-serve.js +407 -0
  53. package/dist/shared-service-images.js +10 -0
  54. package/dist/shared-services.global-setup.js +174 -0
  55. package/dist/shared-session-ledger.js +305 -0
  56. package/dist/shared-setup-entry.js +12 -0
  57. package/dist/shared-source-scan.js +133 -0
  58. package/dist/shared-spec-document.js +303 -0
  59. package/dist/shared-spec-grammar.js +66 -0
  60. package/dist/shared-specifications.js +26 -0
  61. package/dist/shared-specs-modules.js +1033 -0
  62. package/dist/shared-stub-reply.js +167 -0
  63. package/dist/shared-stub-server.js +142 -0
  64. package/dist/shared-temp-root.global-setup.js +73 -0
  65. package/dist/shared-test-baseline.js +26 -0
  66. package/dist/shared-test-section.js +58 -0
  67. package/dist/shared-testcontainers.adapter.js +306 -0
  68. package/dist/shared-tokens.js +55 -0
  69. package/dist/shared-yaml-document.js +119 -0
  70. package/dist/stubs.d.ts +2 -0
  71. package/dist/stubs.js +657 -0
  72. package/dist/temp-root.global-setup.d.ts +7 -0
  73. package/dist/temp-root.global-setup.js +1 -0
  74. package/dist/vitest.d.ts +93 -164
  75. package/dist/vitest.js +382 -550
  76. package/docs/README.md +82 -36
  77. package/docs/concepts/architecture.md +118 -0
  78. package/docs/concepts/conventions.md +333 -0
  79. package/docs/guides/api-specs.md +374 -0
  80. package/docs/guides/cli-specs.md +566 -0
  81. package/docs/guides/component-specs.md +252 -0
  82. package/docs/guides/develop.md +97 -0
  83. package/docs/guides/integration-specs.md +181 -0
  84. package/docs/guides/job-specs.md +180 -0
  85. package/docs/guides/lint-specs.md +174 -0
  86. package/docs/guides/mobile-specs.md +319 -0
  87. package/docs/guides/module-tests.md +149 -0
  88. package/docs/guides/release.md +44 -0
  89. package/docs/guides/spec-documents.md +189 -0
  90. package/docs/guides/website-specs.md +413 -0
  91. package/docs/reference/assertions.md +345 -0
  92. package/docs/reference/glossary.md +20 -0
  93. package/docs/reference/linting.md +483 -0
  94. package/docs/reference/services.md +236 -0
  95. package/docs/reference/stubs.md +500 -0
  96. package/docs/reference/time-and-doubles.md +137 -0
  97. package/docs/reference/tokens.md +258 -0
  98. package/docs/reference/visitor.md +221 -0
  99. package/docs/start/getting-started.md +187 -0
  100. package/docs/start/set-up-a-project.md +228 -0
  101. package/package.json +56 -36
  102. package/schema/spec.schema.json +43 -17
  103. package/dist/appium.adapter.js +0 -489
  104. package/dist/artifacts.js +0 -32
  105. package/dist/fixtures.js +0 -110
  106. package/dist/literate.js +0 -1747
  107. package/dist/match.js +0 -804
  108. package/dist/playwright.adapter.js +0 -438
  109. package/dist/server.js +0 -160
  110. package/dist/stream.js +0 -321
  111. package/dist/substring-warning.js +0 -282
  112. package/docs/01-architecture.md +0 -95
  113. package/docs/02-developing.md +0 -351
  114. package/docs/03-testing.md +0 -495
  115. package/docs/04-operating.md +0 -101
  116. package/docs/05-module-tests.md +0 -152
  117. package/docs/06-integration.md +0 -180
  118. package/docs/07-component.md +0 -246
  119. package/docs/08-website.md +0 -404
  120. package/docs/09-mobile.md +0 -270
  121. package/docs/10-api.md +0 -264
  122. package/docs/11-jobs.md +0 -157
  123. package/docs/12-cli.md +0 -677
  124. package/docs/13-elements.md +0 -248
  125. package/docs/14-assertions.md +0 -314
  126. package/docs/15-tokens.md +0 -253
  127. package/docs/16-contracts.md +0 -438
  128. package/docs/17-services.md +0 -188
  129. package/docs/18-conventions.md +0 -332
  130. package/docs/19-linting.md +0 -512
  131. package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
  132. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
  133. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
  134. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
  135. package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
  136. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
  137. package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
  138. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
  139. package/docs/decisions/_template.md +0 -22
  140. package/docs/reference/classes/Matcher.md +0 -120
  141. package/docs/reference/classes/ProcessHandle.md +0 -368
  142. package/docs/reference/functions/defineContract.md +0 -39
  143. package/docs/reference/functions/defineContracts.md +0 -36
  144. package/docs/reference/functions/disabled.md +0 -28
  145. package/docs/reference/functions/enabled.md +0 -20
  146. package/docs/reference/functions/focused.md +0 -26
  147. package/docs/reference/functions/mockOf.md +0 -79
  148. package/docs/reference/functions/postgres.md +0 -31
  149. package/docs/reference/functions/process.md +0 -33
  150. package/docs/reference/functions/redis.md +0 -28
  151. package/docs/reference/functions/required.md +0 -41
  152. package/docs/reference/functions/selected.md +0 -26
  153. package/docs/reference/functions/sqlite.md +0 -33
  154. package/docs/reference/functions/testId.md +0 -20
  155. package/docs/reference/functions/text.md +0 -40
  156. package/docs/reference/functions/valued.md +0 -28
  157. package/docs/reference/functions/waitUntil.md +0 -46
  158. package/docs/reference/functions/within.md +0 -29
  159. package/docs/reference/index.md +0 -196
  160. package/docs/reference/interfaces/BaseResult.md +0 -87
  161. package/docs/reference/interfaces/CallResult.md +0 -160
  162. package/docs/reference/interfaces/CaptureScope.md +0 -82
  163. package/docs/reference/interfaces/CliResult.md +0 -253
  164. package/docs/reference/interfaces/ContainerAccessor.md +0 -166
  165. package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
  166. package/docs/reference/interfaces/FetchResult.md +0 -180
  167. package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
  168. package/docs/reference/interfaces/HttpResult.md +0 -126
  169. package/docs/reference/interfaces/JsonAccessor.md +0 -46
  170. package/docs/reference/interfaces/PageResult.md +0 -356
  171. package/docs/reference/interfaces/RenderResult.md +0 -72
  172. package/docs/reference/interfaces/RenderedText.md +0 -110
  173. package/docs/reference/interfaces/ResponseAccessor.md +0 -47
  174. package/docs/reference/interfaces/ScreenResult.md +0 -128
  175. package/docs/reference/interfaces/TableAccessor.md +0 -26
  176. package/docs/reference/interfaces/TextAccessor.md +0 -96
  177. package/docs/reference/type-aliases/ApiHandle.md +0 -68
  178. package/docs/reference/type-aliases/ApiSpecification.md +0 -213
  179. package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
  180. package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
  181. package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
  182. package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
  183. package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
  184. package/docs/reference/type-aliases/BrowserPage.md +0 -132
  185. package/docs/reference/type-aliases/BrowserPort.md +0 -51
  186. package/docs/reference/type-aliases/CliEnv.md +0 -11
  187. package/docs/reference/type-aliases/CliHandle.md +0 -80
  188. package/docs/reference/type-aliases/CliInput.md +0 -36
  189. package/docs/reference/type-aliases/CliOutput.md +0 -46
  190. package/docs/reference/type-aliases/CliPort.md +0 -58
  191. package/docs/reference/type-aliases/CliSpecification.md +0 -144
  192. package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
  193. package/docs/reference/type-aliases/ComponentChain.md +0 -156
  194. package/docs/reference/type-aliases/ComponentScenario.md +0 -20
  195. package/docs/reference/type-aliases/ComponentUi.md +0 -47
  196. package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
  197. package/docs/reference/type-aliases/ContainerPort.md +0 -113
  198. package/docs/reference/type-aliases/Contract.md +0 -68
  199. package/docs/reference/type-aliases/ContractInput.md +0 -13
  200. package/docs/reference/type-aliases/ContractRequest.md +0 -95
  201. package/docs/reference/type-aliases/ContractResponder.md +0 -23
  202. package/docs/reference/type-aliases/ContractResponse.md +0 -78
  203. package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
  204. package/docs/reference/type-aliases/Contracts.md +0 -48
  205. package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
  206. package/docs/reference/type-aliases/DatabasePort.md +0 -72
  207. package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
  208. package/docs/reference/type-aliases/DevicePort.md +0 -51
  209. package/docs/reference/type-aliases/DeviceScreen.md +0 -32
  210. package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
  211. package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
  212. package/docs/reference/type-aliases/DomMount.md +0 -21
  213. package/docs/reference/type-aliases/ElementKind.md +0 -27
  214. package/docs/reference/type-aliases/ElementMatch.md +0 -74
  215. package/docs/reference/type-aliases/ElementOptions.md +0 -29
  216. package/docs/reference/type-aliases/ElementRef.md +0 -128
  217. package/docs/reference/type-aliases/ExecOptions.md +0 -37
  218. package/docs/reference/type-aliases/FileAccessor.md +0 -54
  219. package/docs/reference/type-aliases/HonoApp.md +0 -31
  220. package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
  221. package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
  222. package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
  223. package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
  224. package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
  225. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
  226. package/docs/reference/type-aliases/Intercept.md +0 -19
  227. package/docs/reference/type-aliases/InterceptOptions.md +0 -28
  228. package/docs/reference/type-aliases/InterceptScope.md +0 -11
  229. package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
  230. package/docs/reference/type-aliases/JobHandle.md +0 -34
  231. package/docs/reference/type-aliases/JobsHandle.md +0 -57
  232. package/docs/reference/type-aliases/JobsResult.md +0 -16
  233. package/docs/reference/type-aliases/JobsSpecification.md +0 -94
  234. package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
  235. package/docs/reference/type-aliases/LandmarkKind.md +0 -20
  236. package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
  237. package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
  238. package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
  239. package/docs/reference/type-aliases/MatchableRequest.md +0 -61
  240. package/docs/reference/type-aliases/MatcherKind.md +0 -34
  241. package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
  242. package/docs/reference/type-aliases/MobileElementKind.md +0 -14
  243. package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
  244. package/docs/reference/type-aliases/MobileElementRef.md +0 -20
  245. package/docs/reference/type-aliases/MobileHandle.md +0 -65
  246. package/docs/reference/type-aliases/MobileScenario.md +0 -20
  247. package/docs/reference/type-aliases/MobileSpecification.md +0 -51
  248. package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
  249. package/docs/reference/type-aliases/MobileVisitor.md +0 -81
  250. package/docs/reference/type-aliases/MockOfOptions.md +0 -26
  251. package/docs/reference/type-aliases/MockPort.md +0 -10
  252. package/docs/reference/type-aliases/PinnedClock.md +0 -10
  253. package/docs/reference/type-aliases/PostgresOptions.md +0 -32
  254. package/docs/reference/type-aliases/ProcessOptions.md +0 -112
  255. package/docs/reference/type-aliases/RedisOptions.md +0 -20
  256. package/docs/reference/type-aliases/RenderSubject.md +0 -12
  257. package/docs/reference/type-aliases/ScreenNode.md +0 -67
  258. package/docs/reference/type-aliases/ServerPort.md +0 -36
  259. package/docs/reference/type-aliases/ServerResponse.md +0 -46
  260. package/docs/reference/type-aliases/ServerSpec.md +0 -21
  261. package/docs/reference/type-aliases/ServiceHandle.md +0 -216
  262. package/docs/reference/type-aliases/ServiceRecord.md +0 -12
  263. package/docs/reference/type-aliases/SpecDocument.md +0 -84
  264. package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
  265. package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
  266. package/docs/reference/type-aliases/SpecFixture.md +0 -30
  267. package/docs/reference/type-aliases/SpecKind.md +0 -8
  268. package/docs/reference/type-aliases/SpecRun.md +0 -108
  269. package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
  270. package/docs/reference/type-aliases/SpecStream.md +0 -32
  271. package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
  272. package/docs/reference/type-aliases/SqliteOptions.md +0 -36
  273. package/docs/reference/type-aliases/SseEvent.md +0 -58
  274. package/docs/reference/type-aliases/StreamBody.md +0 -61
  275. package/docs/reference/type-aliases/TextFilter.md +0 -22
  276. package/docs/reference/type-aliases/VisitScenario.md +0 -20
  277. package/docs/reference/type-aliases/Visitor.md +0 -215
  278. package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
  279. package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
  280. package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
  281. package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
  282. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
  283. package/docs/reference/variables/anthropic.md +0 -19
  284. package/docs/reference/variables/banner.md +0 -10
  285. package/docs/reference/variables/button.md +0 -10
  286. package/docs/reference/variables/clock.md +0 -16
  287. package/docs/reference/variables/complementary.md +0 -10
  288. package/docs/reference/variables/component.md +0 -10
  289. package/docs/reference/variables/content.md +0 -10
  290. package/docs/reference/variables/contentinfo.md +0 -10
  291. package/docs/reference/variables/dialog.md +0 -10
  292. package/docs/reference/variables/field.md +0 -10
  293. package/docs/reference/variables/form.md +0 -10
  294. package/docs/reference/variables/heading.md +0 -10
  295. package/docs/reference/variables/http.md +0 -40
  296. package/docs/reference/variables/intercept.md +0 -11
  297. package/docs/reference/variables/link.md +0 -10
  298. package/docs/reference/variables/listitem.md +0 -10
  299. package/docs/reference/variables/main.md +0 -10
  300. package/docs/reference/variables/match.md +0 -8
  301. package/docs/reference/variables/navigation.md +0 -10
  302. package/docs/reference/variables/openai.md +0 -21
  303. package/docs/reference/variables/option.md +0 -17
  304. package/docs/reference/variables/region.md +0 -10
  305. package/docs/reference/variables/row.md +0 -10
  306. package/docs/reference/variables/search.md +0 -10
  307. package/docs/reference/variables/specification.md +0 -8
  308. package/docs/reference/variables/status.md +0 -10
  309. package/docs/reference/variables/table.md +0 -10
@@ -0,0 +1,137 @@
1
+ # Time and doubles
2
+
3
+ Two things around a subject are neither a service nor the network: the time it reads, and a collaborator it is handed. This page covers `clock`, which pins time, and `mockOf()`, which doubles an injected port, and the doubles ladder, which says when a double is the right tool at all.
4
+
5
+ ## The clock
6
+
7
+ A value a test samples (`Date.now()`, `new Date()`, `Math.random()`, `randomUUID()`) is different on every run, so it never reaches an expectation (rule `no-sampled-value-in-expectation`). Time is the one of those the framework pins for you, and `clock` is the whole vocabulary:
8
+
9
+ ```typescript
10
+ import { clock } from '@jterrazz/test';
11
+ import { describe, expect, test } from 'vitest';
12
+
13
+ function issueReceipt(total: number): { issuedAt: string; total: number } {
14
+ return { issuedAt: new Date().toISOString(), total };
15
+ }
16
+
17
+ describe('issueReceipt', () => {
18
+ test('stamps the moment the receipt was issued', () => {
19
+ // Given - the calendar pinned for this scope
20
+ using _ = clock.at('2026-03-04T09:30:00Z');
21
+
22
+ // Then - the stamp is the stated instant
23
+ expect(issueReceipt(42)).toEqual({ issuedAt: '2026-03-04T09:30:00.000Z', total: 42 });
24
+ });
25
+ });
26
+ ```
27
+
28
+ The expectation compares what the subject returned, and the instant is a value the test wrote. A test that pins the clock may also read `new Date()` itself, since under the pin that reading is the constant it chose, but such a test checks the clock rather than the subject.
29
+
30
+ | Form | Pins | For |
31
+ | ------------------- | ------------------------------ | --------------------------------------------------------------------------- |
32
+ | `clock.at(iso)` | the calendar (`Date`) | a subject that reads the time: a stamp, an expiry, a greeting |
33
+ | `clock.run(iso?)` | the calendar and the scheduler | a subject whose behaviour is elapsed time: a debounce, a backoff, a poll |
34
+ | `clock.advance(ms)` | none | moving the pinned clock on; under `run()`, firing every timer that came due |
35
+
36
+ **`at()` leaves timers running.** Promises, renders and the framework's own polling keep working, because only `Date` is pinned. **`run()` queues timers instead**, so `setTimeout` and friends fire only when `await clock.advance(ms)` makes them due. That is how a test proves a five-second backoff without waiting five seconds, and without the arbitrary sleep rule `no-sleep` forbids.
37
+
38
+ **Both are scopes.** `using _ = clock.at(...)` gives the real clock back when the scope ends, however it ends. `vi.useFakeTimers()` and `vi.setSystemTime()` are what this replaces: each owns a teardown a test will one day forget, and rule `no-vitest-time-or-global-stub` refuses them, naming `clock`.
39
+
40
+ **One clock per test.** A second scope taken inside the first is refused rather than nested: ending the inner one would end the outer one too, since the only state vitest's timers can give back is the real clock. `clock.advance()` with no clock pinned is refused, and so is an instant `Date` cannot read. An instant is an ISO-8601 string, `'2026-03-04T09:30:00Z'`.
41
+
42
+ A `beforeEach`/`afterEach` pair can hold the scope by hand while a suite migrates:
43
+
44
+ ```typescript
45
+ let pinned: Disposable;
46
+ beforeEach(() => {
47
+ pinned = clock.at('2026-03-04T09:30:00Z');
48
+ });
49
+ afterEach(() => {
50
+ pinned[Symbol.dispose]();
51
+ });
52
+ ```
53
+
54
+ It works, and it is a stopover: `using _ = clock.at(...)` inside the test puts the instant where the test that needs it is read.
55
+
56
+ A timer a subject sets and must release is proven by `vi.getTimerCount()` after `clock.run()`: a count of zero says the `clearTimeout` ran, and kills the mutants of a `finally` or of the clear itself without a directive.
57
+
58
+ ### `.clock()` on a chain
59
+
60
+ A chain pins the same instant for its own action with `.clock(iso)`, released when the action resolves:
61
+
62
+ | On | Pins |
63
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
64
+ | `api`, `job`, `integration` | the `Date` the app, the job or the module reads; they share the test's process |
65
+ | `component` | the page's `Date`, and nothing else: faking a page's timers would stop React's scheduler |
66
+ | `website` (`.visit()` only) | the page's calendar, before its first byte is parsed. A `.fetch()` opens no page and refuses the pairing |
67
+ | `cli` | nothing: a binary is another process. Its instant travels through its own env (`TZ`), or the golden absorbs it with `{{iso8601}}` |
68
+
69
+ A file whose chains all share one instant states it once, on a view: `const morning = api.with({ clock: '2026-03-04T09:30:00Z' })` ([Conventions § What a file shares](../concepts/conventions.md#what-a-file-shares)). A chain whose clock is refused leaves nothing armed: the stubs it armed before the clock are released with the refusal.
70
+
71
+ ## Doubles
72
+
73
+ ### The doubles ladder
74
+
75
+ When a subject needs something it does not own, take the FIRST rung that fits. The list is closed: nothing below the fourth rung is a rung.
76
+
77
+ 1. **The real thing**, in the test's process: a pure module, a Hono app handed to `server`, a component mounted in a real Chromium.
78
+ 2. **A declared service:** `sqlite()`, `postgres()`, `redis()`, `process()` ([Services](services.md)): real, isolated per worker, reset at the start of every chain.
79
+ 3. **A stub:** `.intercept()` on a chain, or `intercept()` around module code ([Stubs](stubs.md)). Strict from the first stub: a request no stub answers fails the test.
80
+ 4. **A typed port double:** `mockOf<Port>()` for an injected interface, `vi.fn<Fn>()` for a function. Asserting a call on one is a boundary observation; asserting only on doubles the test built proves nothing.
81
+
82
+ **Off the ladder**, each with where to go instead:
83
+
84
+ | Instead of | Use |
85
+ | ------------------------------------------------ | ----------------------------------------------- |
86
+ | `vi.stubGlobal('fetch')`, `msw`, `nock`, `sinon` | `intercept()` or `.intercept()`, a stub |
87
+ | `vi.useFakeTimers()`, `vi.setSystemTime()` | `clock` |
88
+ | `toMatchSnapshot()` | a golden, `toMatch(file('./__expected__/...'))` |
89
+ | `vi.mock()` of your own module | inject the port, and double it |
90
+
91
+ A native module a project genuinely cannot inject is the one exception, allowed per specifier with a reason in the `no-module-mock` options ([Linting § Deviating from a rule](linting.md#deviating-from-a-rule)). Why the ladder is closed: [ADR-005](https://github.com/jterrazz/package-test/blob/main/docs/decisions/005-the-doubles-ladder-is-closed.md).
92
+
93
+ ### `mockOf()`
94
+
95
+ `mockOf<Port>()` is the one double a test builds itself, and only a module test does: every other kind declares its doubles, because the test does not hold the subject's wiring.
96
+
97
+ ```typescript
98
+ import { mockOf } from '@jterrazz/test';
99
+ import { describe, expect, test } from 'vitest';
100
+
101
+ type PaymentGateway = { charge(amount: number): Promise<string> };
102
+
103
+ async function checkout(gateway: PaymentGateway, total: number): Promise<string> {
104
+ return `receipt for ${await gateway.charge(total)}`;
105
+ }
106
+
107
+ describe('checkout', () => {
108
+ test('charges the gateway once for the order total', async () => {
109
+ // Given - a gateway that accepts every charge
110
+ const gateway = mockOf<PaymentGateway>();
111
+ gateway.charge.mockResolvedValue('ch_1');
112
+
113
+ // Then - the receipt names the charge, and the gateway was asked for the total
114
+ await expect(checkout(gateway, 42)).resolves.toBe('receipt for ch_1');
115
+ expect(gateway.charge).toHaveBeenCalledWith(42);
116
+ });
117
+ });
118
+ ```
119
+
120
+ It is deep by default: reading a member nobody stubbed hands back another double, however far the port nests. `mockOf<Port>({ deep: false })` is the flat form: every member of the port and nothing beneath them.
121
+
122
+ State the port. `T` has no default, so `mockOf()` with no type argument answers for `object` and the error lands where the double is used, not where it was made. A double a runner hands the app (a class implementing the port, swapped in by the specification) is a test adapter, and lives in `__setup__/<port>.adapter.ts` ([Conventions § The naming recap](../concepts/conventions.md#the-naming-recap)).
123
+
124
+ ### A rejection the subject must handle
125
+
126
+ `vi.fn().mockRejectedValue(error)` cannot prove a rejection is handled: the rejected promise it hands back exists before the subject attaches a handler, and the test passes whether or not the subject ever does. The double is a function that throws when it is called, `async () => { throw error; }`, so the rejection is born inside the subject's own call. To let an `unhandledRejection` surface before the assertion, wait one `setImmediate` tick (`await new Promise((resolve) => setImmediate(resolve))`), never a `setTimeout`, which `no-sleep` refuses.
127
+
128
+ ## Pitfalls
129
+
130
+ - **`vi.setSystemTime` instead of `clock`.** Taken without a scope, it outlasts the test, and the next test in the file inherits a clock nobody declared.
131
+ - **Sleeping to let time pass.** `await new Promise((r) => setTimeout(r, 5000))` waits for real and hides how long the behaviour needs; `clock.run()` and `clock.advance(5000)` state it.
132
+ - **`vi.mock()` to avoid injecting a port.** Every importer of that module gets the double, and the design pressure the injection would have applied is gone.
133
+ - **A double inside an integration spec.** The integration kind exists so the module meets the real thing; a `mockOf()` there is a module test that started a container for nothing.
134
+
135
+ ## Related
136
+
137
+ [Module tests](../guides/module-tests.md) · [Stubs](stubs.md) · [Services](services.md) · [Conventions](../concepts/conventions.md)
@@ -0,0 +1,258 @@
1
+ # Tokens: dynamic values in fixtures
2
+
3
+ Real systems generate UUIDs, timestamps, ports, and paths. The framework handles them with **one unified token grammar** (rule `no-unknown-token`): the same `{{token}}` vocabulary works in every fixture file, and the same vocabulary is available in code as `match.*`. The vocabulary is fixed (21 tokens, defined, tested, and documented in the package), and an unknown token in a fixture is an error, not a silent literal.
4
+
5
+ ## Where tokens work
6
+
7
+ | Fixture kind | Files | Coverage |
8
+ | -------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | HTTP responses | `__expected__/*.http` | **body and headers** |
10
+ | JSON snapshots | `__expected__/*.json` | any string value |
11
+ | Text snapshots | `__expected__/*.txt` (and other text files under `__expected__/`) | anywhere in the text |
12
+ | Spec documents | `<case>.spec.yaml`, beside the spec ([Spec documents](../guides/spec-documents.md)) | what a document asserts (a run's streams and `files:` texts, a response's status, headers and body, a table's cells, a job's `error:`), **never the description, a command, a request or a body**, which are prose and inputs |
13
+
14
+ ```http
15
+ ### __expected__/order-created.http: tokens in a header AND the body
16
+ HTTP/1.1 201 Created
17
+ Location: /orders/{{uuid#order}}
18
+
19
+ { "id": "{{uuid#order}}", "total": "{{number}}", "createdAt": "{{iso8601}}" }
20
+ ```
21
+
22
+ ```text
23
+ ### __expected__/help.txt: tokens in a TEXT snapshot
24
+ shoply v{{semver}}
25
+ Started at {{iso8601}} in {{workdir}}
26
+ Done in {{duration}}
27
+ ```
28
+
29
+ A token in a JSON body is written inside quotes (`"total": "{{number}}"`) because the file must stay JSON, and a quoted numeric token still matches a native JSON number as a whole value: `{{number}}`, `{{int}}`, `{{float}}` and `{{port}}` accept `42` as readily as `"42"`. `{{any}}` matches any type at all.
30
+
31
+ Stream comparisons strip ANSI escape sequences **before** token matching, so fixtures never contain color codes: `.text` is the stripped text, `.raw` the capture as it was.
32
+
33
+ ## The vocabulary: all 21 tokens
34
+
35
+ Every token matches one value of its family at its position. The normative definitions (the exact accepted grammar for each) are shipped and tested inside the package; the table below states each token's semantics and canonical accepted forms.
36
+
37
+ ### Identifiers
38
+
39
+ | Token | Matches | Example value |
40
+ | ---------- | ---------------------------------------------- | -------------------------------------- |
41
+ | `{{uuid}}` | RFC 4122 UUID, `8-4-4-4-12` hexadecimal groups | `3f2c9b6e-8a41-4c8f-9d2a-1b7e5f0c4d3e` |
42
+ | `{{ulid}}` | ULID, 26 Crockford base32 characters | `01J2X3YV5T8Q4R9K7M6N1P0ZSD` |
43
+
44
+ ### Time
45
+
46
+ | Token | Matches | Example value |
47
+ | -------------- | ------------------------------------------------------------- | ---------------------------- |
48
+ | `{{iso8601}}` | Full ISO 8601 timestamp (date + time, `Z` or offset) | `2026-07-16T10:32:05.123Z` |
49
+ | `{{date}}` | Calendar date, `YYYY-MM-DD` | `2026-07-16` |
50
+ | `{{time}}` | Wall-clock time, `HH:MM[:SS]` (seconds and fraction optional) | `10:32:05` |
51
+ | `{{duration}}` | Elapsed-time string as printed by tooling | `340ms`, `1.24s`, `2m`, `3h` |
52
+
53
+ ### Numbers
54
+
55
+ | Token | Matches | Example value |
56
+ | ------------ | -------------------------------------- | ------------- |
57
+ | `{{number}}` | Any numeric value (integer or decimal) | `109`, `4.5` |
58
+ | `{{int}}` | Integer only | `42` |
59
+ | `{{float}}` | Decimal with a fractional part | `0.97` |
60
+
61
+ ### Versions & hashes
62
+
63
+ | Token | Matches | Example value |
64
+ | ------------ | ------------------------------------------------------------------ | --------------------- |
65
+ | `{{semver}}` | Semantic version (`major.minor.patch`, optional pre-release/build) | `9.0.0`, `9.1.0-rc.1` |
66
+ | `{{sha}}` | Commit/content hash digest (hexadecimal, git-style) | `f690adf...` |
67
+ | `{{hex}}` | Hexadecimal string | `deadbeef` |
68
+ | `{{base64}}` | Base64 string (padding included) | `aGVsbG8=` |
69
+
70
+ ### Network
71
+
72
+ | Token | Matches | Example value |
73
+ | ----------- | -------------------------------- | ------------------------------------- |
74
+ | `{{port}}` | TCP/UDP port number (0 to 65535) | `54321` |
75
+ | `{{ip}}` | IP address | `127.0.0.1` |
76
+ | `{{url}}` | Absolute URL with scheme | `https://rates.example.com/v1/latest` |
77
+ | `{{email}}` | Email address | `contact@jterrazz.com` |
78
+
79
+ ### Paths
80
+
81
+ | Token | Matches | Example value |
82
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
83
+ | `{{path}}` | Filesystem path | `/tmp/x/dist/index.js` |
84
+ | `{{workdir}}` | **The exact cwd of the current spec**: the framework knows the temp directory it created, so this is an equality check, not a pattern | `/tmp/jterrazz-test-runs-501/48213-Xk2pQa/spec-command-a1B2c3` |
85
+
86
+ `{{workdir}}` holds the **resolved** cwd, the spelling a child process reports, after any symlink on the way (macOS's `$TMPDIR` sits under `/var`, a link to `/private/var`). The `$WORKDIR` token that `.env()` and a document's `env:` expand is that same resolved path, so a directory the runner hands to the binary comes back in its output matching the token: `.env({ CACHE_DIR: '$WORKDIR/cache' })` is asserted as `{{workdir}}/cache`.
87
+
88
+ ### Generic
89
+
90
+ | Token | Matches |
91
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `{{string}}` | Any string value |
93
+ | `{{any}}` | Anything: always-true **as a whole value** (matches any type: object, number, `null`). Embedded in a frame (`A {{any}} Z`) it only widens the middle; the surrounding literal still has to match, and text on both sides of it on its line holds it to that line |
94
+
95
+ ## `#ref` captures
96
+
97
+ Any token becomes a capture by suffixing `#name`: `{{uuid#order}}`. The **first occurrence captures** the actual value; every subsequent occurrence of the same ref **must be equal**. Refs are scoped to the current spec: one namespace shared by all fixture files and code-side `match.ref()` within that spec, resetting between specs.
98
+
99
+ ```http
100
+ ### __expected__/order-created.http: {{uuid#order}} appears 3 times, all must be equal
101
+ HTTP/1.1 201 Created
102
+ Location: /orders/{{uuid#order}}
103
+
104
+ {
105
+ "id": "{{uuid#order}}",
106
+ "paymentIntent": { "orderId": "{{uuid#order}}" }
107
+ }
108
+ ```
109
+
110
+ A ref that re-occurs with a different value fails with both values in the message: the captured one and the conflicting one.
111
+
112
+ ## `match.*`: the same vocabulary in code
113
+
114
+ Every token has a code mirror for inline assertions (table rows, primarily):
115
+
116
+ ```typescript
117
+ import { match } from '@jterrazz/test';
118
+
119
+ await expect(result.table('orders', { database: 'db' })).toMatch([
120
+ [match.uuid(), 'pending', match.number(), match.iso8601()],
121
+ ]);
122
+ ```
123
+
124
+ `match.uuid()`, `match.ulid()`, `match.iso8601()`, `match.date()`, `match.time()`, `match.duration()`, `match.number()`, `match.int()`, `match.float()`, `match.semver()`, `match.sha()`, `match.hex()`, `match.base64()`, `match.port()`, `match.ip()`, `match.url()`, `match.email()`, `match.path()`, `match.workdir()`, `match.string()`, `match.any()`, plus four code-only forms:
125
+
126
+ | Code-only | Meaning |
127
+ | --------------------------------- | ------------------------------------------------------------------------------------------- |
128
+ | `match.regex(/.../)` | Arbitrary pattern, the escape hatch when no token fits |
129
+ | `match.ref(name)` | Capture on first use, require equality afterwards (same namespace as `{{token#name}}` refs) |
130
+ | `match.ref(name, { not: other })` | Additionally require **inequality** with another captured ref |
131
+ | `match.includes(substring)` | Containment: the value holds the substring; used mostly in a stub's request filter |
132
+
133
+ ```typescript
134
+ // intent.order_id === orders.id, but intent.id ≠ orders.id
135
+ await expect(result.table('orders', { database: 'db' })).toMatch([[match.ref('order'), 'pending']]);
136
+ await expect(result.table('payment_intents', { database: 'db' })).toMatch([
137
+ [match.ref('intent', { not: 'order' }), match.ref('order')],
138
+ ]);
139
+ ```
140
+
141
+ ## Update mode: tokens are preserved
142
+
143
+ `TEST_UPDATE=1` (or `vitest -u`) rewrites fixtures from actual output, but it writes **tokens, not values** (rule `no-volatile-literal`):
144
+
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
+ - 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
+ - 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
+
149
+ Example, starting from actual output:
150
+
151
+ ```text
152
+ shoply v9.1.0
153
+ Started at 2026-07-17T09:12:44.031Z in /tmp/jterrazz-test-runs-501/48213-Xk2pQa/spec-command-x9Y8z7
154
+ Done in 0.83s
155
+ ```
156
+
157
+ with an existing fixture:
158
+
159
+ ```text
160
+ shoply v{{semver}}
161
+ Started at {{iso8601}} in {{workdir}}
162
+ Done in {{duration}}
163
+ ```
164
+
165
+ `TEST_UPDATE=1` leaves the fixture byte-identical: every changed segment was already covered by a token. If the CLI adds a new line `Cache dir: /tmp/shoply-spec-x9y8z7/.cache`, the update writes `Cache dir: {{workdir}}/.cache` (`{{workdir}}` is substituted automatically); a timestamp in a _new_ line would be written literally, for you to tokenize by hand (or it will fail on the next run, which is the signal).
166
+
167
+ The same preservation and `{{workdir}}` substitution apply identically across every fixture kind: a value written in place (below), text snapshots (`__expected__/*.txt`), JSON goldens (`__expected__/*.json`), `.http` response bodies (`__expected__/*.http`), and the streams of a `<case>.spec.yaml` (whose ground, commands and `files:` an update never rewrites). A cwd embedded in a JSON string value is written back as `{{workdir}}` exactly as it is in a text line, so an updated JSON golden matches the next run's (different) cwd instead of pinning a run-specific temp path.
168
+
169
+ ### A value written in place is rewritten in the test
170
+
171
+ A `toMatch(<value>)` is rewritten where it stands, in the test file, the gesture vitest's `toMatchInlineSnapshot` makes: the call is found from the stack of the assertion, and its argument (up to its comma or its closing parenthesis, whatever template, string or nested call it holds) is replaced by the actual result, merged with the tokens the old value had where the result still matches them. A text is written as a template literal indented one level deeper than its call, a json as its JSON, a table one row per line. An empty value, `toMatch('')`, is the one an author leaves to be recorded: outside update mode it fails naming the run that fills it. A value that matches is never touched, and neither is a `{ frozen: true }` one or a `.not`: a negation asserts a mismatch, and records nothing. Every rewrite of one file during a run is laid on the source the run loaded, so a second rewrite lands where its stack says even when a first one moved the lines below it. Under the preset the test file is written when the run ENDS, after its report: vitest frames a failure by reading the file on disk at the line the run loaded, so a file rewritten mid-run would frame a later failure at another test's code. The preset's global setup hands each project a pending folder under `.artifacts/vitest/in-place/` and writes what waits there once the report is printed (in watch mode, when the watcher exits); a run outside the preset writes each file at once. A component's value is rewritten the same way through a server command, the page's stack mapped back to the test's source by the browser server. What update mode writes comes out in the repository's style: an integer of five digits or more is grouped by threes (`1_775_034_000_000`, as the linter's numeric separators ask), and when the run ends each rewritten test file is laid out by the oxfmt the project installs, with the project's own config: its quotes, its keys, its trailing commas. Outside the preset, or where no oxfmt is installed, the file stays as written, and `npx typescript fix` formats it.
172
+
173
+ This is why `transform` on `specification.cli` is only an escape hatch (rule `no-transform-duplicating-tokens`): the token grammar plus ANSI stripping covers the standard sources of nondeterminism, and a transform that merely re-implements standard tokens is refused by `no-transform-duplicating-tokens` ([no-transform-duplicating-tokens](linting.md#no-transform-duplicating-tokens)).
174
+
175
+ ### Frozen fixtures: opting a negative fixture out of the rewrite
176
+
177
+ Update mode rewrites a mismatching fixture with the actual output. That is exactly wrong for a **deliberately-wrong fixture**, one whose _mismatch_ is the behaviour under test (a spec that asserts the diff, or that a missing fixture errors). Under `TEST_UPDATE=1` an unfrozen negative fixture is silently overwritten with the real output, and the assertion stops testing anything.
178
+
179
+ Pass `{ frozen: true }` as the second argument to opt a single fixture out:
180
+
181
+ ```typescript
182
+ // A deliberately-wrong fixture: its diff rendering is the subject.
183
+ expect(() =>
184
+ expect(result.response).toMatch(file('./__expected__/wrong-body.http'), { frozen: true }),
185
+ ).toThrow(/Response mismatch/);
186
+ ```
187
+
188
+ A frozen fixture is **never written** in update mode: a frozen mismatch still throws its diff, and a frozen _missing_ fixture still throws "does not exist" (never created). Its failure says so rather than pointing at update mode: write the file by hand, or drop `{ frozen }` for one `TEST_UPDATE=1` run and put it back. It is available on every golden subject (`result.response`, `result.stdout`/`result.stderr`, `result.json`, `result.directory`, `result.filesystem`). The static rule `negative-golden-frozen` flags a golden `toMatch(file(...))`/`toMatch(directory(...))` wrapped in `expect(() => ...).toThrow()` / `.rejects` that omits it.
189
+
190
+ ### The discipline: update mode writes, it does not judge
191
+
192
+ A rewritten fixture is not a passing test: it is this run's output, made into the expectation. Three steps, in this order, and the middle one is the whole point.
193
+
194
+ 1. **Run it.** `TEST_UPDATE=1 npx vitest --run --project <kind>`, narrowed to the project whose goldens moved, never the whole suite, because a broad update rewrites files nobody looked at.
195
+ 2. **Read the diff.** `git diff` is the review: every line that changed is a behaviour that changed. A golden diff that nobody can explain is a bug that just became the expectation, and this is the only moment it is cheap to catch.
196
+ 3. **Run again, without the flag.** The second run is the one that proves anything. It reads the file as any other run would, and it is what a reviewer's machine will do.
197
+
198
+ The round-trip is not a step the suite performs for you, and deliberately so: update mode writes the file from THIS run's actual value, so re-comparing inside the same run matches by construction and proves nothing. The real round-trip already exists: CI runs the same goldens on another machine, which is exactly the question "does this file still hold when the run is not the one that wrote it?"
199
+
200
+ **The frozen two-pass.** A frozen fixture is never written, so changing one on purpose is two deliberate passes: drop `{ frozen: true }`, run once under `TEST_UPDATE=1`, read the diff, and put the option back in the same commit. The friction is the feature: a negative fixture should cost something to change.
201
+
202
+ **What update mode never touches.** The ground of a `<case>.spec.yaml` (its `description:`, its commands, its `env:`/`serve:`/`fixture:` and its `files:`) is authored, never rewritten; only the expected streams move. Its `files:` are still judged under update, so a document that passes a recorded run is one a normal run passes.
203
+
204
+ **What update mode refuses to record.** An expectation that cannot fail is no expectation. Update mode writes nothing over a result whose call threw: the value of a `.call()` that failed is `null`, and a recorded `toMatch(null)` stays green while every call fails: assert the error, or fix the call. It rewrites a literal only: a `toMatch(` handed a name or a call stands for values the rewrite would replace by the last one it saw (a table's rows are recorded by hand, one by one). And `toMatch('')` on a string (a stream's `.text`, a file's `.content`) is vitest's substring match, true of any text; the rule `no-empty-substring-match` refuses it. A `__requests__/*.http` is an input, not an expectation, and is never written either.
205
+
206
+ ## Known limitations
207
+
208
+ The grammar is deliberately small; three edges are worth knowing:
209
+
210
+ - **No escaping for literal `{{...}}` text.** There is no escape sequence. A `{{uuid}}` written in a fixture is _always_ interpreted as the token; you cannot assert a literal, un-substituted `{{uuid}}` string in expected output. (An unknown name like `{{widget}}` is caught by the conventions checker (rule `no-unknown-token`) as an error: the vocabulary is frozen, so an unknown token is never a usable literal escape.) For genuinely custom shapes, use `match.regex()` in code.
211
+ - **Update-merge pairs one previous line with one actual line.** Placeholder preservation is by **pattern**: the aligned line is tried first, then what is left over is re-paired by matching each remaining placeholder line against each unresolved actual line, in file order. A mid-file insertion is therefore safe: the shifted `{{duration}}` line is recognised where it landed. What stays out of reach: a previous line is spent **once**, so when the output grows a SECOND line the same placeholder could cover, the first keeps the token and the new one is written as a raw value, for you to tokenize.
212
+ - **`{{float}}` cannot require a decimal on a JSON number.** In JSON there is no distinction between `42` and `42.0`, so as a whole JSON value `{{float}}` accepts any finite number (an integer passes). The decimal requirement only holds in **text** contexts (`4.2` matches, `42` does not). This is a JSON limitation, not a token choice.
213
+
214
+ ## Appendix: canonical accepted forms (normative, as implemented)
215
+
216
+ The exact grammar of each token, one tested regular expression per token
217
+ (defined in [`src/expectations/tokens/structural.ts`](https://github.com/jterrazz/package-test/blob/main/src/expectations/tokens/structural.ts), the vocabulary list in `match.ts` beside it). In embedded positions the pattern is
218
+ anchored to its surroundings; as a whole JSON value, numeric tokens also
219
+ accept native JSON numbers.
220
+
221
+ | Token | Accepted form (regex) | Notes |
222
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
223
+ | `{{uuid}}` | `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}` | any RFC 4122 variant |
224
+ | `{{ulid}}` | `[0-9A-HJKMNP-TV-Z]{26}` | Crockford base32, uppercase |
225
+ | `{{iso8601}}` | `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z\|[+-]\d{2}:\d{2})` | date + time, `Z` or offset required |
226
+ | `{{date}}` | `\d{4}-\d{2}-\d{2}` | |
227
+ | `{{time}}` | `\d{2}:\d{2}(:\d{2}(\.\d+)?)?` | seconds and fraction optional |
228
+ | `{{duration}}` | `\d+(\.\d+)?(ms\|s\|m\|h)` | strict: `ms`, `s`, `m`, `h` suffixes only |
229
+ | `{{number}}` | `-?\d+(\.\d+)?([eE][+-]?\d+)?` | JSON numbers also accepted as whole values |
230
+ | `{{int}}` | `-?\d+` | whole JSON value: `Number.isInteger` |
231
+ | `{{float}}` | `-?\d+\.\d+` | whole JSON value: any finite number |
232
+ | `{{semver}}` | `\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?` | |
233
+ | `{{sha}}` | `[0-9a-f]{7,64}` | 7 to 64 lowercase hex chars (short SHA up to SHA-512) |
234
+ | `{{hex}}` | `[0-9a-fA-F]+` | |
235
+ | `{{base64}}` | `[A-Za-z0-9+/]+={0,2}` | |
236
+ | `{{port}}` | `\d{1,5}` | range-checked to 0 to 65535 (embedded placeholders reject out-of-range values like `99999`) |
237
+ | `{{ip}}` | `(\d{1,3}\.){3}\d{1,3}` | IPv4 only for now |
238
+ | `{{url}}` | `https?://[^\s"'<>]+` | http(s) schemes only |
239
+ | `{{email}}` | `[^\s@"'<>]+@[^\s@"'<>]+\.[^\s@"'<>]+` | |
240
+ | `{{path}}` | `\.{0,2}/[^\s"'<>]*` | absolute (`/...`) or relative (`./...`, `../...`) |
241
+ | `{{workdir}}` | none (exact string equality with the spec's cwd, realpath form) | never matches when the framework does not know the cwd |
242
+ | `{{string}}` | any string; embedded: `[^\n]*?` (single line) | |
243
+ | `{{any}}` | anything; embedded: `[\s\S]*?` (crosses lines) where its line holds no text on one side of it (alone on a line, opening it or closing it); `[^\n]*?` where text frames it on its line (`"msg":"{{any}}",`) | |
244
+
245
+ ## Pitfalls
246
+
247
+ - **Inventing a token.** The vocabulary is fixed; `{{timestamp}}` in a fixture is an error (rule `no-unknown-token`). Use `{{iso8601}}`, or `match.regex()` in code for genuinely custom shapes.
248
+ - **Expecting `{{workdir}}` to be a pattern.** It matches the _exact_ cwd of the current spec; a path from another spec's cwd fails.
249
+ - **Assuming refs span the suite.** Ref scope is the current spec; `{{uuid#order}}` in two different tests are unrelated captures.
250
+ - **Using `{{any}}` where a precise token exists.** `{{any}}` matches garbage too; every widening of a fixture weakens the spec.
251
+ - **Reading "always-true" as unframed.** `{{any}}` is only unconditional as the _whole_ value. Embedded (`A {{any}} Z`), the frame around it must still match, and a non-string/number actual cannot satisfy an embedded token at all.
252
+ - **Hand-writing values that update mode would tokenize.** Run `TEST_UPDATE=1` first, then tighten: placeholders you wrote once are preserved forever after.
253
+ - **Escaping into `transform` for tokenizable noise.** Only application noise _not_ covered by the grammar justifies a transform (rule `no-transform-duplicating-tokens`); one a token already names is refused by `no-transform-duplicating-tokens`.
254
+ - **Leaving a deliberately-wrong fixture unfrozen.** A negative test whose golden matcher asserts a mismatch (wrapped in `expect(() => ...).toThrow()` / `.rejects`) must pass `{ frozen: true }`; otherwise `TEST_UPDATE=1` overwrites the wrong fixture with the real output and the assertion silently stops testing anything (rule `negative-golden-frozen`).
255
+
256
+ ## Related
257
+
258
+ [Assertions](assertions.md) · [API specs](../guides/api-specs.md) · [CLI specs](../guides/cli-specs.md)