@gfdelarue/testkit 0.5.0 → 0.6.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 (95) hide show
  1. package/README.md +136 -110
  2. package/dist/app/bugs/draft.js +2 -2
  3. package/dist/app/bugs/guide.js +1 -1
  4. package/dist/app/bugs/record.js +2 -2
  5. package/dist/app/campaigns/index.js +2 -2
  6. package/dist/app/destroy/index.js +1 -1
  7. package/dist/app/discover/index.js +2 -2
  8. package/dist/app/environments/static-values.js +1 -1
  9. package/dist/app/environments/supplied.js +1 -1
  10. package/dist/app/inspect/index.js +1 -1
  11. package/dist/app/lint/native.js +1 -1
  12. package/dist/app/lint/options.js +1 -1
  13. package/dist/app/run/engine-orchestration.js +1 -1
  14. package/dist/app/run/environment.js +1 -1
  15. package/dist/app/run/orchestrator.js +1 -1
  16. package/dist/app/run/request.js +1 -1
  17. package/dist/app/run/run-finalization.js +1 -1
  18. package/dist/app/run/task-batches.js +1 -1
  19. package/dist/app/run/worker-loop.js +1 -1
  20. package/dist/app/status/model.js +1 -1
  21. package/dist/app/status/runs.js +1 -1
  22. package/dist/app/typecheck/index.js +4 -4
  23. package/dist/cli/command-catalog.js +1 -1
  24. package/dist/cli/command-flags.js +1 -1
  25. package/dist/cli/commands/artifacts.js +2 -2
  26. package/dist/cli/commands/discover.js +1 -1
  27. package/dist/cli/commands/guard/unsafe-entrypoint.js +1 -1
  28. package/dist/cli/commands/run.js +2 -2
  29. package/dist/cli/commands/typecheck.js +1 -1
  30. package/dist/cli/commands/workspace.js +1 -1
  31. package/dist/config/authoring/database-fixture-discovery.js +1 -0
  32. package/dist/config/authoring/index.js +1 -1
  33. package/dist/config/loader/config-loader.js +1 -1
  34. package/dist/config/loader/decode-discovery.js +1 -1
  35. package/dist/config/loader/discover-tests.js +1 -1
  36. package/dist/config/loader/discovery.js +2 -2
  37. package/dist/config/loader/file-metadata.js +1 -1
  38. package/dist/config/loader/groups.js +2 -1
  39. package/dist/config/loader/load.js +1 -1
  40. package/dist/config/loader/managed-configs.js +1 -1
  41. package/dist/config/loader/owned-discovery.js +1 -1
  42. package/dist/config/loader/runtime-isolation.js +1 -1
  43. package/dist/config/loader/skip-config.js +1 -1
  44. package/dist/database/admin.js +2 -2
  45. package/dist/database/configured-steps.js +1 -1
  46. package/dist/domain/identifiers.js +1 -1
  47. package/dist/domain/retired-test-types.js +1 -1
  48. package/dist/domain/suite-selection.js +1 -1
  49. package/dist/domain/test-types.js +1 -1
  50. package/dist/engines/catalog.js +1 -1
  51. package/dist/engines/definitions.js +1 -1
  52. package/dist/engines/environment/index.js +1 -1
  53. package/dist/engines/installations/node-vitest.js +1 -1
  54. package/dist/engines/installations/playwright.js +1 -1
  55. package/dist/engines/managed-module-resolution.js +6 -2
  56. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  57. package/dist/engines/node-vitest/adapter/driver.js +2 -2
  58. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  59. package/dist/engines/node-vitest/adapter/workspace.js +8 -24
  60. package/dist/engines/node-vitest/inventory/index.js +2 -2
  61. package/dist/engines/node-vitest/inventory/list-files.js +3 -3
  62. package/dist/engines/node-vitest/runtime/data/index.js +1 -1
  63. package/dist/engines/node-vitest/runtime/data/report.js +1 -1
  64. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  65. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  66. package/dist/engines/playwright/adapter/adapter.js +1 -1
  67. package/dist/engines/playwright/adapter/config.js +2 -2
  68. package/dist/engines/playwright/runtime/managed-test.js +1 -0
  69. package/dist/environment/lifecycle.js +1 -1
  70. package/dist/environment/run-environment.js +1 -1
  71. package/dist/evidence/artifact-config.js +1 -1
  72. package/dist/evidence/maintenance.js +1 -0
  73. package/dist/evidence/read-run.js +1 -1
  74. package/dist/kernel/known-keys.js +1 -0
  75. package/dist/process/execa-bundle.json +1 -1
  76. package/dist/repo/fingerprint-policy.js +1 -1
  77. package/dist/steps/configured-step-testkit-modules.js +1 -1
  78. package/dist/toolchains/index.js +2 -2
  79. package/dist/toolchains/vitest/index.js +1 -1
  80. package/dist/types/config.d.ts +7 -86
  81. package/dist/types/environment.d.ts +6 -0
  82. package/dist/types/node-vitest-data.d.ts +2 -218
  83. package/dist/types/workspace.d.ts +27 -11
  84. package/docs/bug-records.md +3 -8
  85. package/docs/engines.md +63 -17
  86. package/docs/release-notes.md +2116 -0
  87. package/docs/test-types.md +19 -69
  88. package/package.json +2 -5
  89. package/testkit-config-schema.md +65 -128
  90. package/dist/app/lint/rules/data.js +0 -1
  91. package/dist/config/authoring/runtime.js +0 -1
  92. package/dist/engines/node-vitest/runtime/contract/index.js +0 -1
  93. package/dist/engines/node-vitest/runtime/data/http.js +0 -1
  94. package/dist/engines/node-vitest/runtime/data/runner.js +0 -1
  95. package/dist/types/node-vitest-contract.d.ts +0 -75
@@ -11,12 +11,15 @@ so does any Node-side test that reads its environment from `process.env` or
11
11
  [Plain Vitest test files](../README.md#plain-vitest-test-files) for which files
12
12
  Testkit runs.
13
13
 
14
- Typed files carry one of three product-level test types in their filename
15
- suffix:
14
+ A plain Vitest test records data-flow evidence, such as metrics, sequences,
15
+ records, and budgets, with `dataFlow(context)` from
16
+ `@gfdelarue/testkit/node-vitest/data`. Recording evidence gives the test no
17
+ type and no setup. See
18
+ [Record data-flow evidence](../README.md#record-data-flow-evidence).
19
+
20
+ Typed files carry one product-level test type in their filename suffix:
16
21
 
17
22
  - `ui`: browser-level UI checks for user-visible pages and interactions.
18
- - `contract`: Node-backed service/runtime contract checks that need Node APIs but are not data-flow evidence suites.
19
- - `data`: data-flow and data-contract checks for throughput, payload shape, conversation/request traces, and cost/latency budgets.
20
23
 
21
24
  Consumers choose the type that describes the product behavior and declare the
22
25
  execution engine in each file's `defineFile(...)` metadata. Testkit validates
@@ -24,12 +27,10 @@ that pair against its engine catalog and resolves the exact runner profile.
24
27
 
25
28
  The current catalog is deliberately explicit:
26
29
 
27
- | Test type | Engine | Runner profile |
28
- | ------------------ | ------------- | ---------------------- |
29
- | none, a plain file | `node-vitest` | `node-vitest` |
30
- | `data` | `node-vitest` | `node-vitest-data` |
31
- | `contract` | `node-vitest` | `node-vitest-contract` |
32
- | `ui` | `playwright` | `playwright-ui` |
30
+ | Test type | Engine | Runner profile |
31
+ | ------------------ | ------------- | --------------- |
32
+ | none, a plain file | `node-vitest` | `node-vitest` |
33
+ | `ui` | `playwright` | `playwright-ui` |
33
34
 
34
35
  Every typed file must import `defineFile` from the root package and export one
35
36
  direct metadata declaration:
@@ -37,7 +38,7 @@ direct metadata declaration:
37
38
  ```ts
38
39
  import { defineFile } from "@gfdelarue/testkit";
39
40
 
40
- export const testkit = defineFile({ engine: "node-vitest" });
41
+ export const testkit = defineFile({ engine: "playwright" });
41
42
  ```
42
43
 
43
44
  The engine is not inferred from the suffix, imports, or repo configuration. A
@@ -46,9 +47,9 @@ error. The profile is derived from the validated pair and is carried unchanged
46
47
  through scheduling, results, and immutable run evidence.
47
48
 
48
49
  Run selection by `--file`, `--group`, `--type`, or `--suite` narrows the files
49
- whose engine metadata is read and validated. A `--type` that names test types
50
- and typed suite selectors select typed files only; `--type all` includes plain
51
- files. Testkit uses discovered paths, canonical type
50
+ whose engine metadata is read and validated. `--type ui` and typed suite
51
+ selectors select typed files only, `--type plain` selects plain files only, and
52
+ `--type all` selects both. Testkit uses discovered paths, canonical type
52
53
  suffixes, and suite names to select candidates; each selected typed file must
53
54
  still declare a valid engine. Invalid metadata in an unselected file
54
55
  does not block that focused run. Root configuration, service dependencies, and
@@ -56,11 +57,7 @@ group skip, requirement, and isolation selectors remain validated against
56
57
  the complete discovered project. `discover`, `doctor`, and `typecheck` validate
57
58
  metadata across the whole project.
58
59
 
59
- Canonical filename suffixes:
60
-
61
- - `*.ui.testkit.ts`
62
- - `*.contract.testkit.ts`
63
- - `*.data.testkit.ts`
60
+ The canonical filename suffix is `*.ui.testkit.ts`.
64
61
 
65
62
  UI tests may import Playwright's public browser primitives directly from the
66
63
  managed native package:
@@ -72,56 +69,9 @@ import { expect, test } from "@playwright/test";
72
69
  export const testkit = defineFile({ engine: "playwright" });
73
70
  ```
74
71
 
75
- Data suites import data-flow primitives from Testkit:
76
-
77
- ```ts
78
- import { defineFile } from "@gfdelarue/testkit";
79
- import { dataScenario } from "@gfdelarue/testkit/node-vitest/data";
80
- import { expect } from "vitest";
81
-
82
- export const testkit = defineFile({ engine: "node-vitest" });
83
- ```
84
-
85
- Contract suites import Node-backed contract primitives from Testkit:
86
-
87
- ```ts
88
- import { defineFile } from "@gfdelarue/testkit";
89
- import { contractScenario } from "@gfdelarue/testkit/node-vitest/contract";
90
- import { expect } from "vitest";
91
-
92
- export const testkit = defineFile({ engine: "node-vitest" });
93
- ```
94
-
95
- Use `contract` when the behavior under test is a service/runtime boundary that
96
- needs Node APIs, fixtures, file IO, or richer protocol checks, but does not need
97
- Testkit data-flow evidence. Contract files are native Vitest suites:
98
- `contractScenario(...)` provides the managed Testkit contract context, while
99
- direct public runner APIs remain available for native behavior such as
100
- `test.skip(...)` and `test.todo(...)`. Contract suites run as Node processes
101
- managed by Testkit and should talk to configured service URLs.
102
-
103
- Use `data` when the behavior under test is the shape, cost, volume, latency, or
104
- movement of data through a real product flow. A data suite is not a generic
105
- Vitest migration target: write `dataScenario(...)` cases that record Testkit
106
- structured data-flow evidence with the provided data context, such as metrics,
107
- budgets, traces, fixtures, HTTP payload measurements, cache behavior, LLM usage,
108
- actors, systems, records, transitions, external calls, and reconciliation
109
- evidence.
110
- Data scenarios use Vitest directly for assertions, hooks, and mocking.
111
- `dataScenario(...)` owns case registration and evidence: lint reports native
112
- registration imports, and the runtime rejects cases registered any other way.
113
- Weak bookkeeping such as a single `count(...)`, `event(...)`, or `finding(...)`
114
- is useful as supporting detail, but it is not enough by itself to make a suite a
115
- data-flow suite.
116
-
117
- Data suites run as Node processes managed by Testkit's `node-vitest` engine and
118
- should use configured service URLs for data-flow evidence. If a check requires
119
- service-local loopback, `serviceContextUrl`, DOM rendering, browser automation,
120
- or raw app-server process access, use a plain Vitest test, `contract`, or `ui` instead.
121
-
122
- Use a plain Vitest test file for unit tests and other Node-side checks that need
123
- no Testkit context, `contract` for typed service or protocol contracts, and `ui`
124
- when the browser is part of the assertion. Plain Vitest files can exercise HTTP,
72
+ Use a plain Vitest test file for unit tests, other Node-side checks, and
73
+ service or protocol contracts, and `ui` when the browser is part of the
74
+ assertion. Plain Vitest files can exercise HTTP,
125
75
  persistence, and service-spanning flows without a type label.
126
76
 
127
77
  The retired `@gfdelarue/testkit/runtime`, `@gfdelarue/testkit/data`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfdelarue/testkit",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Test orchestration platform for product acceptance and QA workflows",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -50,10 +50,6 @@
50
50
  "types": "./dist/types/node-vitest-data.d.ts",
51
51
  "default": "./dist/engines/node-vitest/runtime/data/index.js"
52
52
  },
53
- "./node-vitest/contract": {
54
- "types": "./dist/types/node-vitest-contract.d.ts",
55
- "default": "./dist/engines/node-vitest/runtime/contract/index.js"
56
- },
57
53
  "./workspace": {
58
54
  "types": "./dist/types/workspace.d.ts",
59
55
  "default": "./dist/app/inspect/index.js"
@@ -88,6 +84,7 @@
88
84
  "dist/",
89
85
  "docs/bug-records.md",
90
86
  "docs/engines.md",
87
+ "docs/release-notes.md",
91
88
  "docs/test-types.md",
92
89
  "testkit-config-schema.md"
93
90
  ],
@@ -94,12 +94,10 @@ literal values `"node-vitest"` or `"playwright"`.
94
94
  The filename suffix determines the test type, and each engine and test type
95
95
  pair runs under exactly one runner profile:
96
96
 
97
- | Test type | Engine | Profile |
98
- | ------------------ | ------------- | ---------------------- |
99
- | none, a plain file | `node-vitest` | `node-vitest` |
100
- | `data` | `node-vitest` | `node-vitest-data` |
101
- | `contract` | `node-vitest` | `node-vitest-contract` |
102
- | `ui` | `playwright` | `playwright-ui` |
97
+ | Test type | Engine | Profile |
98
+ | ------------------ | ------------- | --------------- |
99
+ | none, a plain file | `node-vitest` | `node-vitest` |
100
+ | `ui` | `playwright` | `playwright-ui` |
103
101
 
104
102
  For a typed file, missing metadata, dynamic values, and unsupported
105
103
  engine/test-type pairs are discovery errors. There is no default-engine or legacy-runner compatibility
@@ -121,20 +119,29 @@ runtime-policy modules under `src/` just to cooperate with managed runs.
121
119
 
122
120
  ## Top-Level Fields
123
121
 
122
+ The top-level keys are `areas`, `artifacts`, `bugs`, `discovery`, `execution`,
123
+ `fingerprints`, `groups`, `lint`, `redact`, `services`, `toolchains`, and `ui`.
124
+ Testkit refuses a config with any other top-level key. The error names the key
125
+ and the closest known key when one is close.
126
+
124
127
  ### `execution`
125
128
 
126
129
  Default execution settings for the repo.
127
130
 
128
- - `taskConcurrency`: maximum concurrent test tasks across the run (default `1`)
131
+ - `taskConcurrency`: maximum concurrent test tasks across the run (default `1`).
132
+ A typed file, a batch of typed files, or one Vitest invocation of plain files
133
+ is one task. Plain files in one invocation run with Vitest's file
134
+ parallelism, which the Vitest config's `fileParallelism` and `maxWorkers`
135
+ control.
129
136
  - `fileTimeoutSeconds`
130
137
 
131
- `execution.workers` and `presets.nodeNext({ workers })` remain deprecated aliases
132
- through 0.2.x and will be removed in 0.3.0. Use `taskConcurrency` in new config.
138
+ `execution.workers` and `presets.nodeNext({ workers })` are deprecated aliases.
139
+ Use `taskConcurrency` in new config.
133
140
  If both names are present, their values must match.
134
141
 
135
- `--task-concurrency` overrides the configured value. The CLI alias `--workers`
136
- also remains available through 0.2.x; contradictory CLI aliases are rejected
137
- before applying CLI precedence. Contradictory config aliases are rejected even
142
+ `--task-concurrency` overrides the configured value. The deprecated CLI alias
143
+ `--workers` also works; contradictory CLI aliases are rejected before applying
144
+ CLI precedence. Contradictory config aliases are rejected even
138
145
  when the CLI supplies an override.
139
146
 
140
147
  Run documents keep the configured `run.workers` field. Observed
@@ -149,8 +156,9 @@ helpers like `waitFor()` consume this budget automatically.
149
156
  Optional `testkit lint` settings. Omit it to run the default rules.
150
157
 
151
158
  - `rules`: map of rule name to `true`, `false`, or an options object.
152
- `engineImports`, `configImports`, `dataSuiteContract`, and `uiSpecShape` default to enabled; `unitTestBoundaries` and
153
- `testkitBoundary` default to disabled.
159
+ `engineImports`, `configImports`, and `uiSpecShape` default to enabled; `unitTestBoundaries` and
160
+ `testkitBoundary` default to disabled. `dataSuiteContract` was removed in
161
+ 0.6.0, and `testkit lint` rejects a config that sets it.
154
162
  - Rule options: `enabled` (default `true`), `include`, `exclude`. `testkitBoundary`
155
163
  also accepts `allowed` and `trackedOnly`.
156
164
  - `ui`: `maxLines` (default `220`) and `maxTests` (default `8`) for UI suites.
@@ -158,16 +166,11 @@ Optional `testkit lint` settings. Omit it to run the default rules.
158
166
  Enabled-rule findings and syntax errors fail the command. There are no severity
159
167
  levels. The rules report:
160
168
 
161
- - `engineImports`: a value import the suite's engine can't load, such as Playwright
162
- in a contract suite. Type-only imports
169
+ - `engineImports`: a value import the suite's engine can't load, such as Vitest
170
+ in a UI suite. Type-only imports
163
171
  are allowed.
164
172
  - `configImports`: `testkit.config.ts` importing a repo-local `__testkit__`
165
173
  helper module.
166
- - `dataSuiteContract`: a data suite that doesn't register its cases through
167
- `dataScenario` from `@gfdelarue/testkit/node-vitest/data`, registers them
168
- through Vitest's `test`, `it`, `describe`, `suite`, or `bench`, or records
169
- only bookkeeping such as `count(...)`, `event(...)`, and `finding(...)`
170
- without product data-flow evidence such as `http(...)` or `record(...)`.
171
174
  - `uiSpecShape`: a UI suite longer than `ui.maxLines` lines or with more than
172
175
  `ui.maxTests` tests.
173
176
  - `unitTestBoundaries`: test doubles, module replacement, `process.env` reads,
@@ -277,7 +280,7 @@ runs. The backend falls back to `TESTKIT_BACKEND_BASE_URL` and then
277
280
  ### `services`
278
281
 
279
282
  Map of service name to service config. A service describes what Testkit can
280
- start. With `groups` set, a service owns no test files.
283
+ start. It owns no test files; [`groups`](#groups) assigns them.
281
284
 
282
285
  ### `groups`
283
286
 
@@ -324,21 +327,21 @@ Files that no group's roots claim belong to the implicit group `root`, which
324
327
  uses no service. `root` is reserved. With no `testkit.config.ts`, every file
325
328
  belongs to `root`, and `groups: {}` gives every file to `root`.
326
329
 
327
- With `groups` set, Testkit ignores a service's `discovery` and rejects a
328
- service's `skip`, `requirements`, or `runtimeIsolation` with a message naming
329
- the group field to use.
330
+ Testkit assigns no files by a service's `discovery`. With `groups` set, it
331
+ rejects a service's `discovery`, `skip`, `requirements`, or `runtimeIsolation`
332
+ with a message naming the group field to use. The `discovery` that
333
+ `database.fixture()` adds by default is the exception.
330
334
 
331
- #### Legacy ownership
335
+ #### Configs without `groups`
332
336
 
333
- A config with `services` and no `groups` key uses legacy ownership. Each
334
- service is a group of the same name that uses that service. It owns the typed
335
- `*.testkit.ts` files under its `local.cwd` and `discovery.roots`, and its
336
- `skip`, `requirements`, and `runtimeIsolation` apply to them. A typed file
337
- outside every service root belongs to `root`. Legacy ownership never claims a
338
- plain Vitest test file; those belong to `root` unless declared groups claim
339
- them. When legacy ownership gives a
340
- file to a service, Testkit prints a notice on stderr. Legacy ownership will be
341
- removed; declare `groups` instead.
337
+ Before 0.6.0, a config with `services` and no `groups` key gave each service
338
+ the typed `*.testkit.ts` files under its `local.cwd` and `discovery.roots`. Such
339
+ a config now fails to load when a service's directories hold a typed file. The
340
+ error lists a `groups` entry for each of those services, with its `roots`,
341
+ `uses`, and any `exclude`, so the files keep their services. `status`,
342
+ `destroy`, `cleanup`, and `db schema` still run on such a config. A group also
343
+ owns the plain Vitest files under its roots, which a service never did. When no
344
+ service's directories hold a typed file, every file belongs to `root`.
342
345
 
343
346
  ### `toolchains`
344
347
 
@@ -378,48 +381,12 @@ download mode fails on a musl host before downloading anything.
378
381
  - `package.json#packageManager`
379
382
  - `package.json#engines.npm`
380
383
 
381
- ### `profiles.http`
382
-
383
- Named HTTP profiles for Node/Vitest data suites, selected through
384
- `flow.http({ profile })`.
384
+ ### `profiles`
385
385
 
386
- Node/Vitest data profiles may carry `auth`, `headers`, and
387
- `rawHeaders` functions. `auth.setup` or `auth.setupAsync`
388
- returns a session bundle of named actors; `headers` maps the bundle and the
389
- requested actor to request headers.
390
-
391
- ```ts
392
- interface SessionBundle {
393
- actors: Record<string, { session: { jwt: string } }>;
394
- primaryActor: string;
395
- }
396
-
397
- profiles: {
398
- http: {
399
- default: {
400
- auth: {
401
- async setupAsync({ env, http }): Promise<SessionBundle> {
402
- const login = await http.post(`${env.BASE}/api/v1/auth/login`, body);
403
- const jwt = login.cookies?.session?.[0]?.value;
404
- if (!jwt) throw new Error("Login response did not set the session cookie");
405
- return { actors: { primary: { session: { jwt } } }, primaryActor: "primary" };
406
- },
407
- headers(setupData): Record<string, string> {
408
- const bundle = setupData as SessionBundle | null;
409
- return bundle
410
- ? { Authorization: `Bearer ${bundle.actors[bundle.primaryActor].session.jwt}` }
411
- : {};
412
- },
413
- },
414
- },
415
- },
416
- }
417
- ```
418
-
419
- Repository profile setup data is typed as `unknown`. The example gives its
420
- test-owned setup result a name and uses that type in the corresponding header
421
- builder. Suite-specific functions can also be passed through
422
- `flow.http({ profile: "default", auth: { setupAsync, headers } })`.
386
+ Testkit 0.6.0 removed HTTP profiles. A config that sets `profiles` fails to
387
+ load. A test logs in with `fetch` or its own client against the URL from
388
+ `@gfdelarue/testkit/environment`, as
389
+ [Record data-flow evidence](README.md#record-data-flow-evidence) shows.
423
390
 
424
391
  ## Service Fields
425
392
 
@@ -594,10 +561,9 @@ to the matching `{db*:api}` token. `env.values` entries override expanded names.
594
561
  ### `envFile` / `envFiles`
595
562
 
596
563
  Optional extra env files loaded into the declared service/task environment for
597
- migrations, seeds, services, and every execution engine. Node/Vitest data and contract contexts expose task values through
598
- immutable `context.env`; and managed Playwright contexts expose them through
599
- `readPlaywrightContext().env`. Direct managed-Node `process.env.KEY` reads are a
600
- non-enumerable compatibility overlay, not the consumer-input contract.
564
+ migrations, seeds, services, and every execution engine. Node/Vitest tests read
565
+ task values from `process.env`, and managed Playwright contexts expose them
566
+ through `readPlaywrightContext().env`.
601
567
 
602
568
  Testkit reads a service's env files only when a selected group uses the service
603
569
  or a used service depends on it. Loading the config reads none of them, so a
@@ -766,19 +732,20 @@ Changing the resolved Node or npm version invalidates prepared artifacts, so
766
732
 
767
733
  ### `skip`
768
734
 
769
- Skip rules for a group's files, set as `groups.<name>.skip`. Under legacy
770
- ownership a service's `skip` applies to the files it owns. Skipped files remain
735
+ Skip rules for a group's files, set as `groups.<name>.skip`. Skipped files remain
771
736
  visible in run artifacts with `status: "skipped"` and the configured reason.
772
737
 
773
738
  Prefer file-local `export const testkit = defineFile({ ... })` metadata
774
- for per-file skips and locks. Repo-level `skip` is mainly for broad suite-level
775
- policy.
739
+ for per-file skips and locks in a typed file. Repo-level `skip` is mainly for
740
+ broad suite-level policy. A plain Vitest file has no file-local metadata, so its
741
+ skips, locks, and isolation go in `skip.files`, `requirements.files`, and
742
+ `runtimeIsolation.files`.
776
743
 
777
744
  ```ts
778
745
  skip: {
779
746
  files: [
780
747
  {
781
- path: "__testkit__/billing/billing.contract.testkit.ts",
748
+ path: "src/billing/billing.test.ts",
782
749
  reason: "Billing is currently unavailable",
783
750
  },
784
751
  ],
@@ -796,12 +763,13 @@ skip: {
796
763
  - `files[].reason`
797
764
  - required non-empty explanation recorded in artifacts
798
765
  - `suites[].selector`
799
- - existing suite selector syntax, including typed selectors like `contract:health`
800
- and `ui:billing`
766
+ - existing suite selector syntax, including typed selectors like `ui:billing`
801
767
  - `suites[].reason`
802
768
  - required non-empty explanation recorded in artifacts
803
769
 
804
- Use `--ignore-skip-rules` on the CLI to run matched files anyway.
770
+ A run that skips a selected file fails, even when every other file passes, and
771
+ `--allow-empty` doesn't change that. Use `--ignore-skip-rules` on the CLI to run
772
+ skipped files anyway.
805
773
 
806
774
  ### `runtime`
807
775
 
@@ -838,17 +806,16 @@ runtime: {
838
806
 
839
807
  ### `requirements`
840
808
 
841
- Task lock rules for a group's files, set as `groups.<name>.requirements`. Under
842
- legacy ownership a service's `requirements` apply to the files it owns.
809
+ Task lock rules for a group's files, set as `groups.<name>.requirements`.
843
810
 
844
811
  ```ts
845
812
  requirements: {
846
813
  suites: [
847
- { selector: "contract:payments", locks: ["payments-sandbox-webhook"] },
814
+ { selector: "ui:checkout", locks: ["payments-sandbox-webhook"] },
848
815
  ],
849
816
  files: [
850
817
  {
851
- path: "src/api/routes/__testkit__/payout-webhook.contract.testkit.ts",
818
+ path: "src/api/routes/payout-webhook.test.ts",
852
819
  locks: ["payments-sandbox-webhook"],
853
820
  },
854
821
  ],
@@ -861,7 +828,7 @@ wait for each other. Use a lock for a resource that can't be duplicated, never
861
828
  for tests that share rows.
862
829
 
863
830
  - `suites[].selector`
864
- - typed selector like `contract:health` or `ui:billing`
831
+ - typed selector like `ui:billing`
865
832
  - `suites[].locks`
866
833
  - named locks required by matching files
867
834
  - `files[].path`
@@ -872,15 +839,14 @@ for tests that share rows.
872
839
  ### `runtimeIsolation`
873
840
 
874
841
  What a group's files share with each other in a run, set as
875
- `groups.<name>.runtimeIsolation`. Under legacy ownership a service's
876
- `runtimeIsolation` applies to the files it owns. A file can set its own with
842
+ `groups.<name>.runtimeIsolation`. A file can set its own with
877
843
  `defineFile({ runtimeIsolation: "resetBefore" })`.
878
844
 
879
845
  ```ts
880
846
  runtimeIsolation: {
881
847
  default: "shared",
882
- suites: [{ selector: "contract:migrations", policy: "resetBefore" }],
883
- files: [{ path: "__testkit__/orders/purge.contract.testkit.ts", policy: "resetBefore" }],
848
+ suites: [{ selector: "ui:admin", policy: "resetBefore" }],
849
+ files: [{ path: "src/orders/purge.test.ts", policy: "resetBefore" }],
884
850
  }
885
851
  ```
886
852
 
@@ -957,44 +923,15 @@ Typed suites are discovered from the filesystem:
957
923
  - a file belongs to the group with the deepest matching root in
958
924
  `groups.<name>.roots`
959
925
  - files that no group claims belong to the implicit group `root`
960
- - under legacy ownership, service groups take their roots from `local.cwd` and
961
- `discovery.roots`
962
-
963
- Supported suffixes:
964
926
 
965
- - `*.contract.testkit.ts`
966
- - `*.data.testkit.ts`
967
- - `*.ui.testkit.ts`
927
+ The supported suffix is `*.ui.testkit.ts`.
968
928
 
969
929
  Every discovered typed file must also export `defineFile({ engine: "..." })`
970
930
  from the root package. The suffix supplies `testType`; the required engine declaration
971
931
  supplies `engineId`; and Testkit derives `profileId` from the catalog. None of
972
932
  those identities are silently rewritten later in the run.
973
933
 
974
- `*.contract.testkit.ts` is a Node-backed service/runtime contract suite type. It
975
- declares `engine: "node-vitest"`, uses
976
- `@gfdelarue/testkit/node-vitest/contract`, runs through
977
- `node-vitest-contract`, and is the right place for checks that need Node APIs
978
- but are not data-flow evidence suites. Contract files are native Vitest suites:
979
- `contractScenario(...)` provides managed Testkit context when needed, while
980
- direct public Vitest runner APIs remain valid.
981
-
982
- `*.data.testkit.ts` is a Testkit-owned data-flow/data-contract suite type. It
983
- declares `engine: "node-vitest"`, uses
984
- `@gfdelarue/testkit/node-vitest/data`, records evidence through
985
- `dataScenario(...)`, and runs through `node-vitest-data`. Data suites may use
986
- non-registering public Vitest APIs such as `expect`, but `dataScenario(...)`
987
- owns case registration. Ordinary Node/Vitest assertions, DOM/browser checks,
988
- and service-local loopback checks belong in the appropriate non-data suite
989
- type.
990
-
991
- Examples:
992
-
993
- - `src/api/routes/__testkit__/auth/me.contract.testkit.ts`
994
- - `src/api/routes/__testkit__/runtime.contract.testkit.ts`
995
- - `src/api/routes/__testkit__/agent-flow.data.testkit.ts`
996
- - `src/db/__testkit__/sessions/count-type.contract.testkit.ts`
997
- - `frontend/__testkit__/navigation/navigation.ui.testkit.ts`
934
+ Example: `frontend/__testkit__/navigation/navigation.ui.testkit.ts`.
998
935
 
999
936
  `testkit discover` exposes that inventory directly without running suites:
1000
937
 
@@ -1 +0,0 @@
1
- import{AST_NODE_TYPES as r}from"@typescript-eslint/types";import{binding as p,memberName as w,stringValue as E}from"../policy.js";const T=new Set(["addFinding","actor","budget","bytes","count","duration","event","externalCall","expectMetric","expectSequence","fixture","finding","http","metric","money","ratio","reconcile","record","recordCache","recordHttpRequest","recordLlmUsage","recordRpc","sequence","sequenceBudget","system","transition"]),N=new Set(["actor","bytes","duration","externalCall","http","money","ratio","reconcile","record","recordCache","recordHttpRequest","recordLlmUsage","recordRpc","sequence","system","transition"]),f=new Set(["bench","describe","it","suite","test","BenchmarkRunner","TestRunner","EvaluatedModules"]);export function createDataPolicy({context:o,file:C,report:n}){if(!C.endsWith(".data.testkit.ts"))return{};const d=new Set,h=new Set,v=new Map,g=new Map;let y=!1,b=!1;const u=(e,t,a="vitest-runner-import")=>{n(e,a,t,`Data suites cannot use ${t} for Vitest case registration; declare cases with dataScenario(...)`)},x=e=>{for(const t of o.sourceCode.getAncestors(e).reverse())if(t.type===r.ArrowFunctionExpression||t.type===r.FunctionExpression){const a=g.get(t);if(a)return a}},m=e=>e.startsWith("vitest/")&&e!=="vitest/package.json"||e==="@gfdelarue/testkit/node-vitest/contract",k=e=>{E(e)?.includes("{serviceContextUrl:")&&n(e,"service-namespace-url","serviceContextUrl","Data suites must not use serviceContextUrl placeholders because they run outside the deployed service namespace")};return{ImportDeclaration(e){const t=e.source.value;if(t==="@playwright/test"&&n(e,"browser-runtime-import",t,"Browser/runtime driving belongs in ui suites, not data suites"),(t.startsWith("@testing-library/")||t==="jsdom"||t==="happy-dom")&&n(e,"dom-runtime-import","dom-runtime","DOM/rendering tests belong in ui suites, not data suites"),e.importKind==="type")return;const a=e.specifiers.filter(s=>s.type!==r.ImportSpecifier||s.importKind!=="type");if(!(e.specifiers.length&&!a.length)){m(t)&&u(e,t),t==="@gfdelarue/testkit/node-vitest/data"&&(y=!0);for(const s of a){const i=o.sourceCode.getDeclaredVariables(e).find(l=>l.name===s.local.name);if(t==="vitest"&&s.type===r.ImportNamespaceSpecifier&&i&&h.add(i),s.type!==r.ImportSpecifier)continue;const c=s.imported.type===r.Identifier?s.imported.name:s.imported.value;t==="vitest"&&f.has(c)&&u(s,c),t==="@gfdelarue/testkit/node-vitest/data"&&c==="dataScenario"&&i&&d.add(i)}}},ExportNamedDeclaration(e){if(!e.source||e.exportKind==="type")return;const t=e.specifiers.filter(a=>a.exportKind!=="type");if(!(e.specifiers.length&&!t.length)&&(m(e.source.value)&&u(e,e.source.value),e.source.value==="vitest"))for(const a of t){const s=a.local.type===r.Identifier?a.local.name:a.local.value;f.has(s)&&u(a,s)}},ExportAllDeclaration(e){e.exportKind!=="type"&&m(e.source.value)&&u(e,e.source.value)},MemberExpression(e){if(e.object.type!==r.Identifier)return;const t=p(o,e.object),a=w(e);t&&h.has(t)&&a&&f.has(a)&&u(e,a,"vitest-runner-call")},CallExpression(e){const t=e.callee.type===r.Identifier?p(o,e.callee):null;if(t&&d.has(t)){b=!0;const i=e.arguments.at(-1);if(!i||i.type!==r.ArrowFunctionExpression&&i.type!==r.FunctionExpression){n(e,"missing-scenario-callback","dataScenario","dataScenario(...) must provide a callback that receives the Testkit data context");return}const c=i.params[0]?.type===r.AssignmentPattern?i.params[0].left:i.params.at(0),l=c?.type===r.Identifier?o.sourceCode.getDeclaredVariables(i).find(D=>D.name===c.name):null;if(!l){n(e,"missing-data-context","dataScenario","dataScenario callbacks must accept the Testkit data context and record data evidence");return}const S={callback:i,context:l,contextName:l.name,methods:new Set,assertion:!1};v.set(e,S),g.set(i,S);return}const a=o.sourceCode.getText(e.callee);e.callee.type===r.MemberExpression&&a.includes(".serviceContextUrl")&&n(e,"service-namespace-url","serviceContextUrl","Data suites run in the managed Node.js runner, not inside a service namespace; use baseUrl/public endpoints or move service-namespace checks to contract");const s=x(e);if(s&&(/^(expect|assert)(?:\(|\.)/u.test(a)&&(s.assertion=!0),e.callee.type===r.MemberExpression&&e.callee.object.type===r.Identifier&&p(o,e.callee.object)===s.context)){const i=w(e.callee);i&&T.has(i)&&s.methods.add(i)}},"CallExpression:exit"(e){const t=v.get(e);if(!t)return;const{callback:a,contextName:s,methods:i,assertion:c}=t;i.size?[...i].some(l=>N.has(l))||(n(a,"weak-data-evidence",s,"Data scenarios must record product data-flow evidence such as http, bytes, sequence, record, transition, system, externalCall, recordCache, recordLlmUsage, or reconcile; ordinary Node/Vitest-style checks belong in *.contract.testkit.ts with @gfdelarue/testkit/node-vitest/contract"),c&&n(a,"vitest-shaped-data-suite",s,"This dataScenario looks like an ordinary assertion test with weak data bookkeeping; move Node-backed contract checks to *.contract.testkit.ts or model the product data flow with structured evidence")):n(a,"missing-data-evidence",s,"Data scenarios must record Testkit-owned data-flow evidence with the data context, such as http, bytes, sequence, record, transition, system, externalCall, recordCache, recordLlmUsage, or reconcile")},ThrowStatement(e){const t=x(e);t&&(t.assertion=!0)},Literal:k,TemplateLiteral:k,"Program:exit"(e){y?d.size?b||n(e,"missing-data-scenario","dataScenario","Data suites must declare at least one dataScenario(...)"):n(e,"missing-data-scenario-import","dataScenario","Data suites must import dataScenario from @gfdelarue/testkit/node-vitest/data"):n(e,"missing-data-import","@gfdelarue/testkit/node-vitest/data","Data suites must import dataScenario from @gfdelarue/testkit/node-vitest/data")}}}
@@ -1 +0,0 @@
1
- let o=null;export function registerRepoConfig(n){o=n??null}export function getRepoConfig(){return o}export function clearRepoConfig(){o=null}export function resolveHttpProfile(n){if(!n)return null;const t=getRepoConfig();if(!t)return null;const r=t.profiles?.http?.[n];if(!r)throw new Error(`Unknown testkit HTTP profile "${n}"`);return r}
@@ -1 +0,0 @@
1
- import{createHttpMethods as x}from"../../../../config/authoring/http-methods.js";import b from"node:fs";import{parseRuntimeFixtureManifestEntries as g,resolveRuntimeFixtureEntry as N}from"../../../contracts/fixture-manifest.js";import{readNodeVitestRuntimeInput as d}from"../runtime-input.js";import{expect as T,test as C}from"vitest";import{isUnknownRecord as s}from"../../../../kernel/unknown-values.js";export{T as expect};const w=Object.freeze({}),E=Object.freeze({env:w});export function contractScenario(t,e,n){const{options:r,run:i}=M(e,n);C(t,async()=>{await i(O())},r.timeoutMs||r.timeout)}export function readContractContext(){const t=d();if(t===null)return E;if(t.profileId!=="node-vitest-contract")throw new Error("Node/Vitest contract API requires node-vitest-contract runtime input");const e={...t.context,env:t.environment,...Object.hasOwn(t,"setupData")?{setupData:t.setupData}:{}};if(!A(e))throw new Error("Node/Vitest contract runtime context is invalid");return Object.freeze(e)}export function createHttpClient(t={}){const e=(t.baseUrl||readContractContext().baseUrl||"").replace(/\/+$/u,"");if(!e)throw new Error("createHttpClient(...) requires baseUrl or the managed Node/Vitest contract context baseUrl");const n=t.defaultHeaders||{"Content-Type":"application/json"};async function r(i,c,y,h={},a={}){const u=i.toUpperCase()||"GET",f=j(y,{contentTypeJson:a.contentTypeJson!==!1}),o=await fetch(`${e}${l(c)}`,{method:u,headers:{...f.body==null?{}:n,...h},body:f.body,signal:a.signal}),p=await o.text();return{body:p,headers:R(o.headers),json(){return JSON.parse(p)},method:u,path:l(c),status:o.status,url:o.url}}return{baseUrl:e,...x(r),request:r}}function O(){const t=readContractContext();return Object.freeze({...t,fixture(e){return S(e)},http(e={}){return createHttpClient(e)}})}function S(t){const e=U(t),n=b.readFileSync(e.absolutePath);return{buffer(){return Buffer.from(n)},contentType:e.contentType,filename:e.filename,json(){return JSON.parse(n.toString("utf8"))},path:e.path,sha256:e.sha256,sizeBytes:e.sizeBytes,text(r="utf8"){return n.toString(r)}}}function U(t){const e=q(t,"fixture name"),n=z();return N(n.entries,e)}function z(){const t=d();return t===null?{entries:[]}:{entries:g(t.fixtures,"Node/Vitest contract fixture manifest")}}function j(t,e){if(t==null)return{body:void 0};if(typeof t=="string")return{body:t};if(t instanceof Uint8Array){const n=new Uint8Array(t.byteLength);return n.set(t),{body:n}}if(e.contentTypeJson)return{body:JSON.stringify(t)};throw new TypeError("Non-JSON contract request bodies must be strings or Uint8Array values")}function l(t){const e=t||"/";return e.startsWith("/")?e:`/${e}`}function R(t){const e={};for(const[n,r]of t.entries())e[n.toLowerCase()]=r;return e}function M(t,e){if(typeof t=="function")return{options:{},run:t};if(typeof e!="function")throw new Error("contractScenario(name, options?, run) requires a callback");return{options:t,run:e}}function q(t,e){const n=typeof t=="string"?t.trim():"";if(!n)throw new Error(`${e} is required`);return n}function A(t){return!s(t)||t.engineId!=="node-vitest"||t.profileId!=="node-vitest-contract"||!k(t.env)||"runner"in t||t.baseUrl!==void 0&&t.baseUrl!==null&&typeof t.baseUrl!="string"||t.serviceName!==void 0&&typeof t.serviceName!="string"?!1:I(t.task)}function I(t){return s(t)?!("type"in t)&&t.testType==="contract"&&m(t.file)&&J(t.id)&&m(t.suiteName):!1}function m(t){return t===void 0||typeof t=="string"}function J(t){return t===void 0||typeof t=="string"||typeof t=="number"}function k(t){return s(t)&&Object.values(t).every(e=>typeof e=="string")}
@@ -1 +0,0 @@
1
- import{stringFromUnknown as _}from"./primitives.js";import{createHttpMethods as b}from"../../../../config/authoring/http-methods.js";import{resolveHttpProfile as F}from"../../../../config/authoring/runtime.js";import{readNodeVitestRuntimeInput as G}from"../runtime-input.js";import{isUnknownRecord as l}from"../../../../kernel/unknown-values.js";export function createHttpClient(t={}){const e=(t.baseUrl??readDataContext().baseUrl??"").replace(/\/+$/u,"");if(!e)throw new Error("createHttpClient(...) requires baseUrl or the managed Node/Vitest data context baseUrl");const n=t.recorder||t.trace||null,o=t.defaultHeaders||{"Content-Type":"application/json"},r=t.sessionBundle||null,i=t.defaultActor||r?.primaryActor||null,c=typeof t.getHeaders=="function"?t.getHeaders:null,s=typeof t.getRawHeaders=="function"?t.getRawHeaders:c;async function u(a,f,w,d,E,k={},g={}){O(r,a);const A=w.trim()===""?"GET":w.toUpperCase(),D=`${e}${x(d)}`,H=v(E,{contentTypeJson:g.contentTypeJson!==!1}),$={actor:a&&r?r.actors[a]??null:null,actorName:a,sessionBundle:r},S={...H.body==null?{}:o,...m(f?s:c,$),...k},L=Date.now(),h=await fetch(D,{method:A,headers:S,body:H.body,signal:g.signal}),T=await h.text(),P=Date.now()-L,M=C(h.headers),J={body:T,cookies:V(h.headers),durationMs:P,headers:M,json(){return JSON.parse(T)},method:A,path:x(d),status:h.status,url:D};return n&&n.recordHttpRequest({bodyBytes:it(H.body),durationMs:P,method:A,path:x(d),requestHeaders:S,responseBytes:Buffer.byteLength(T),status:h.status}),J}const p=y(null,!0),R=b(p),U=Object.assign(p,{as:B,get:R.get,post:R.post}),N=y(i,!1);return{...b(N),baseUrl:e,headers(a={}){return{...m(c,{actorName:i,actor:j(r,i),sessionBundle:r}),...a}},rawHeaders(a={}){return{...m(s,{actorName:i,actor:j(r,i),sessionBundle:r}),...a}},raw:U,rawReq:U,request:N,as:I};function y(a,f){return(w,d,E,k,g)=>u(a,f,w,d,E,k,g)}function I(a){O(r,a);const f=y(a,!1),w=B(a);return{...b(f),headers(d={}){return{...m(c,{actorName:a,actor:r?r.actors[a]??null:null,sessionBundle:r}),...d}},rawHeaders(d={}){return{...m(s,{actorName:a,actor:r?r.actors[a]??null:null,sessionBundle:r}),...d}},raw:w,rawReq:w,request:f}}function B(a){const f=y(a,!0);return Object.assign(f,b(f))}}export function readDataContext(){const t=G();if(t===null)return nt;if(t.profileId!=="node-vitest-data")throw new Error("Node/Vitest data API requires node-vitest-data runtime input");return et({...t.context,env:t.environment,...Object.hasOwn(t,"setupData")?{setupData:t.setupData}:{}})}export async function createDataHttpClient(t={}){if(!t.profile)return createHttpClient(t);const e=F(t.profile);if(!e)throw new Error(`Data HTTP profile "${t.profile}" is unavailable because no repo config is registered`);const n=Y(e,t),o=X(n),r=await W(n.auth,o,t.profile),i=createHttpClient({...t,baseUrl:o.BASE,...r?.primaryActor?{defaultActor:r.primaryActor}:{},getHeaders(s){return{...n.auth?.headers?.(r,{actor:s.actorName,env:o})??{},...q(n.headers,r,o,s.actorName)}},getRawHeaders(s){return q(n.rawHeaders,r,o,s.actorName)},...r?{sessionBundle:r}:{}}),c=Q(r,i);return{...i,actor:c.actor,actors:c.actors,env:o}}function K(t){return t??null}function Y(t,e){return{...t,...e,auth:e.auth??K(t.auth),headers:e.headers??t.headers,rawHeaders:e.rawHeaders??t.rawHeaders,env:e.env??t.env}}async function W(t,e,n){const o={env:e,http:Z(),json:ot};let r=null;if(typeof t?.setupAsync=="function"?r=await t.setupAsync(o):typeof t?.setup=="function"&&(r=t.setup(o)),r==null)return null;if(!ft(r))throw new Error(`Data HTTP profile "${n??""}" auth setup must return a session bundle with actors and primaryActor`);return r}function q(t,e,n,o){return typeof t!="function"?{}:t(e,{actor:o,env:n})??{}}function X(t){const n={BASE:readDataContext().baseUrl||process.env.BASE_URL},o={...t.env||n,rawEnv:process.env};if(!o.BASE)throw new Error("Data HTTP profile requires BASE_URL or the managed Node/Vitest data context baseUrl");return{...o,BASE:o.BASE,rawEnv:process.env}}function Q(t,e){const n=Object.keys(t?.actors??{}),o=n.map(s=>{const u=t?.actors[s]??null,p=e.as(s);return{email:u?.email||null,headers:p.headers(),index:u?.actorIndex??0,key:s,name:u?.name||null,organizationKey:u?.organizationKey||null,organizationName:u?.organizationName||null,rawHeaders:p.rawHeaders(),rawReq:p.rawReq,req:p,session:u?.session||null}}),r=new Map(o.map(s=>[s.key,s])),i=t?.primaryActor||null,c=i&&r.get(i)||null;return{actor:c,actors:{names:n,primary:c,get(s){const u=r.get(s);if(!u)throw new Error(`Unknown testkit actor "${s}"`);return u},has(s){return r.has(s)},list(){return o.slice()}}}}function Z(){async function t(e,n,o,r={}){const i=st(r),c=await fetch(n,{method:e,headers:i,body:e==="GET"?void 0:at(o)});return{body:await c.text(),cookies:V(c.headers),headers:C(c.headers),status:c.status}}return{del(e,n,o){return t("DELETE",e,n,o)},get(e,n){return t("GET",e,null,n)},patch(e,n,o){return t("PATCH",e,n,o)},post(e,n,o){return t("POST",e,n,o)},put(e,n,o){return t("PUT",e,n,o)}}}function m(t,e){return typeof t!="function"?{}:t(e)}function O(t,e){if(!(!e||!t)&&!Object.hasOwn(t.actors,e))throw new Error(`Unknown testkit actor "${e}"`)}function x(t){const e=t||"/";return e.startsWith("/")?e:`/${e}`}function v(t,e={}){return t==null?{body:void 0}:typeof t=="string"?{body:t}:t instanceof Uint8Array?{body:Uint8Array.from(t)}:t instanceof URLSearchParams||t instanceof FormData||t instanceof Blob?{body:t}:e.contentTypeJson===!1?{body:z(t)}:{body:JSON.stringify(t)}}function C(t){const e={};for(const[n,o]of t.entries())e[n]=o;return e}function V(t){const e=typeof t.getSetCookie=="function"?t.getSetCookie():tt(t.get("set-cookie")),n={};for(const o of e){const[r]=o.split(";"),i=r.indexOf("=");if(i<=0)continue;const c=r.slice(0,i).trim(),s=r.slice(i+1).trim();n[c]=[{value:s}]}return n}function tt(t){return t?t.split(/,(?=\s*[^;,]+=)/u).map(e=>e.trim()).filter(Boolean):[]}function et(t){if(!l(t))throw new Error("Node/Vitest data context must be an object");if(t.engineId!=="node-vitest")throw new Error('Node/Vitest data context.engineId must be "node-vitest"');if(t.profileId!=="node-vitest-data")throw new Error('Node/Vitest data context.profileId must be "node-vitest-data"');if(!dt(t.env))throw new Error("Node/Vitest data context.env must be a string environment record");if("runner"in t)throw new Error("Node/Vitest data context.runner is not supported");const e={...t,env:t.env};if(t.baseUrl!==void 0&&t.baseUrl!==null&&typeof t.baseUrl!="string")throw new Error("Node/Vitest data context.baseUrl must be a string");if(t.serviceName!==void 0&&typeof t.serviceName!="string")throw new Error("Node/Vitest data context.serviceName must be a string");if(!l(t.task))throw new Error("Node/Vitest data context.task must be an object");{const n=t.task.id;if(n!==void 0&&typeof n!="string"&&typeof n!="number")throw new Error("Node/Vitest data context.task.id is invalid");for(const o of["file","suiteName"])if(t.task[o]!==void 0&&typeof t.task[o]!="string")throw new Error(`Node/Vitest data context.task.${o} must be a string`);if(t.task.testType!=="data")throw new Error('Node/Vitest data context.task.testType must be "data"');if("type"in t.task)throw new Error("Node/Vitest data context.task.type is not supported");e.task={...typeof t.task.file=="string"?{file:t.task.file}:{},...n===void 0?{}:{id:n},...typeof t.task.suiteName=="string"?{suiteName:t.task.suiteName}:{},testType:t.task.testType}}return e.engineId=t.engineId,e.profileId=t.profileId,Object.freeze(e)}const rt=Object.freeze({}),nt=Object.freeze({env:rt});function ot(t){if(!l(t)||typeof t.body!="string")throw new Error("Data HTTP profile response body must be a string");return JSON.parse(t.body)}function st(t){if(t==null)return{};if(!l(t))throw new Error("Data HTTP profile request options must be an object");return t.headers===void 0?{}:ct(t.headers,"Data HTTP profile request headers")}function at(t){if(t!=null)return z(t)}function it(t){return t===void 0?0:typeof t=="string"?Buffer.byteLength(t):t instanceof Blob?t.size:t instanceof URLSearchParams?Buffer.byteLength(t.toString()):t instanceof ArrayBuffer||ArrayBuffer.isView(t)?t.byteLength:0}function j(t,e){return e&&t?.actors?t.actors[e]??null:null}function z(t){return typeof t=="string"||t instanceof Blob||t instanceof FormData||t instanceof URLSearchParams||t instanceof ArrayBuffer?t:ArrayBuffer.isView(t)?Uint8Array.from(new Uint8Array(t.buffer,t.byteOffset,t.byteLength)):_(t)}function ct(t,e){if(!l(t))throw new Error(`${e} must be an object`);const n={};for(const[o,r]of Object.entries(t))n[o]=ut(r,`${e}.${o}`);return n}function ut(t,e){if(typeof t!="string"||t.length===0)throw new Error(`${e} must be a non-empty string`);return t}function ft(t){return l(t)&&l(t.actors)&&(t.primaryActor===null||typeof t.primaryActor=="string")}function dt(t){return l(t)&&Object.values(t).every(e=>typeof e=="string")}
@@ -1 +0,0 @@
1
- import{TestRunner as a}from"vitest";import{isDataScenarioCase as o}from"./scenario.js";export default class s extends a{onCollected(t){for(const r of t)for(const e of r.tasks)if(!o(e))throw new Error(`Data suites must register every case with dataScenario: ${e.name}`)}}
@@ -1,75 +0,0 @@
1
- import { expect } from 'vitest';
2
-
3
- export interface HttpMethods<Response, Extra extends unknown[]> {
4
- get: (path: string, ...extra: Extra) => Response;
5
- delete: (path: string, body?: unknown, ...extra: Extra) => Response;
6
- post: (path: string, body?: unknown, ...extra: Extra) => Response;
7
- put: (path: string, body?: unknown, ...extra: Extra) => Response;
8
- patch: (path: string, body?: unknown, ...extra: Extra) => Response;
9
- }
10
- export interface ContractScenarioOptions {
11
- timeout?: number;
12
- timeoutMs?: number;
13
- }
14
- export interface ContractHttpRequestOptions extends Record<string, unknown> {
15
- contentTypeJson?: boolean;
16
- signal?: AbortSignal;
17
- }
18
- export interface ContractHttpResponse {
19
- body: string;
20
- headers: Record<string, string>;
21
- json(): unknown;
22
- method: string;
23
- path: string;
24
- status: number;
25
- url: string;
26
- }
27
- export interface ContractHttpClientOptions {
28
- baseUrl?: string;
29
- defaultHeaders?: Record<string, string>;
30
- }
31
- export interface ContractHttpClient extends HttpMethods<Promise<ContractHttpResponse>, [
32
- headers?: Record<string, string>,
33
- options?: ContractHttpRequestOptions
34
- ]> {
35
- baseUrl: string;
36
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>, options?: ContractHttpRequestOptions): Promise<ContractHttpResponse>;
37
- }
38
- export interface ContractFixture {
39
- buffer(): Buffer;
40
- contentType: string;
41
- filename: string;
42
- json(): unknown;
43
- path: string;
44
- sha256: string;
45
- sizeBytes: number;
46
- text(encoding?: BufferEncoding): string;
47
- }
48
- export interface ContractRuntimeContext extends Record<string, unknown> {
49
- baseUrl?: string | null;
50
- engineId?: "node-vitest";
51
- readonly env: Readonly<Record<string, string>>;
52
- profileId?: "node-vitest-contract";
53
- serviceName?: string;
54
- task?: {
55
- file?: string;
56
- id?: number | string;
57
- suiteName?: string;
58
- testType: "contract";
59
- };
60
- }
61
- export interface ContractScenarioContext extends ContractRuntimeContext {
62
- fixture(name: string): ContractFixture;
63
- http(options?: ContractHttpClientOptions): ContractHttpClient;
64
- }
65
- export type ContractScenarioRun = (context: ContractScenarioContext) => unknown;
66
- export declare function contractScenario(name: string, run: ContractScenarioRun): void;
67
- export declare function contractScenario(name: string, options: ContractScenarioOptions, run: ContractScenarioRun): void;
68
- export declare function readContractContext(): ContractRuntimeContext;
69
- export declare function createHttpClient(options?: Readonly<ContractHttpClientOptions>): ContractHttpClient;
70
-
71
- export {
72
- expect,
73
- };
74
-
75
- export {};