@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.
- package/README.md +136 -110
- package/dist/app/bugs/draft.js +2 -2
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/record.js +2 -2
- package/dist/app/campaigns/index.js +2 -2
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/discover/index.js +2 -2
- package/dist/app/environments/static-values.js +1 -1
- package/dist/app/environments/supplied.js +1 -1
- package/dist/app/inspect/index.js +1 -1
- package/dist/app/lint/native.js +1 -1
- package/dist/app/lint/options.js +1 -1
- package/dist/app/run/engine-orchestration.js +1 -1
- package/dist/app/run/environment.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/request.js +1 -1
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/task-batches.js +1 -1
- package/dist/app/run/worker-loop.js +1 -1
- package/dist/app/status/model.js +1 -1
- package/dist/app/status/runs.js +1 -1
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-flags.js +1 -1
- package/dist/cli/commands/artifacts.js +2 -2
- package/dist/cli/commands/discover.js +1 -1
- package/dist/cli/commands/guard/unsafe-entrypoint.js +1 -1
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/commands/typecheck.js +1 -1
- package/dist/cli/commands/workspace.js +1 -1
- package/dist/config/authoring/database-fixture-discovery.js +1 -0
- package/dist/config/authoring/index.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/decode-discovery.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery.js +2 -2
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/groups.js +2 -1
- package/dist/config/loader/load.js +1 -1
- package/dist/config/loader/managed-configs.js +1 -1
- package/dist/config/loader/owned-discovery.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/database/admin.js +2 -2
- package/dist/database/configured-steps.js +1 -1
- package/dist/domain/identifiers.js +1 -1
- package/dist/domain/retired-test-types.js +1 -1
- package/dist/domain/suite-selection.js +1 -1
- package/dist/domain/test-types.js +1 -1
- package/dist/engines/catalog.js +1 -1
- package/dist/engines/definitions.js +1 -1
- package/dist/engines/environment/index.js +1 -1
- package/dist/engines/installations/node-vitest.js +1 -1
- package/dist/engines/installations/playwright.js +1 -1
- package/dist/engines/managed-module-resolution.js +6 -2
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/driver.js +2 -2
- package/dist/engines/node-vitest/adapter/evidence.js +2 -2
- package/dist/engines/node-vitest/adapter/workspace.js +8 -24
- package/dist/engines/node-vitest/inventory/index.js +2 -2
- package/dist/engines/node-vitest/inventory/list-files.js +3 -3
- package/dist/engines/node-vitest/runtime/data/index.js +1 -1
- package/dist/engines/node-vitest/runtime/data/report.js +1 -1
- package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/config.js +2 -2
- package/dist/engines/playwright/runtime/managed-test.js +1 -0
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/run-environment.js +1 -1
- package/dist/evidence/artifact-config.js +1 -1
- package/dist/evidence/maintenance.js +1 -0
- package/dist/evidence/read-run.js +1 -1
- package/dist/kernel/known-keys.js +1 -0
- package/dist/process/execa-bundle.json +1 -1
- package/dist/repo/fingerprint-policy.js +1 -1
- package/dist/steps/configured-step-testkit-modules.js +1 -1
- package/dist/toolchains/index.js +2 -2
- package/dist/toolchains/vitest/index.js +1 -1
- package/dist/types/config.d.ts +7 -86
- package/dist/types/environment.d.ts +6 -0
- package/dist/types/node-vitest-data.d.ts +2 -218
- package/dist/types/workspace.d.ts +27 -11
- package/docs/bug-records.md +3 -8
- package/docs/engines.md +63 -17
- package/docs/release-notes.md +2116 -0
- package/docs/test-types.md +19 -69
- package/package.json +2 -5
- package/testkit-config-schema.md +65 -128
- package/dist/app/lint/rules/data.js +0 -1
- package/dist/config/authoring/runtime.js +0 -1
- package/dist/engines/node-vitest/runtime/contract/index.js +0 -1
- package/dist/engines/node-vitest/runtime/data/http.js +0 -1
- package/dist/engines/node-vitest/runtime/data/runner.js +0 -1
- package/dist/types/node-vitest-contract.d.ts +0 -75
package/docs/test-types.md
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
-
| `
|
|
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: "
|
|
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.
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
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
|
],
|
package/testkit-config-schema.md
CHANGED
|
@@ -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
|
-
| `
|
|
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 })`
|
|
132
|
-
|
|
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
|
|
136
|
-
also
|
|
137
|
-
|
|
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`,
|
|
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
|
|
162
|
-
in a
|
|
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.
|
|
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
|
-
|
|
328
|
-
service's `skip`, `requirements`, or `runtimeIsolation`
|
|
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
|
-
####
|
|
335
|
+
#### Configs without `groups`
|
|
332
336
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
382
|
-
|
|
383
|
-
Named HTTP profiles for Node/Vitest data suites, selected through
|
|
384
|
-
`flow.http({ profile })`.
|
|
384
|
+
### `profiles`
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
598
|
-
|
|
599
|
-
`readPlaywrightContext().env`.
|
|
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`.
|
|
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
|
|
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: "
|
|
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 `
|
|
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
|
-
|
|
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`.
|
|
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: "
|
|
814
|
+
{ selector: "ui:checkout", locks: ["payments-sandbox-webhook"] },
|
|
848
815
|
],
|
|
849
816
|
files: [
|
|
850
817
|
{
|
|
851
|
-
path: "src/api/routes/
|
|
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 `
|
|
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`.
|
|
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: "
|
|
883
|
-
files: [{ path: "
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|