@elench/testkit 0.1.55 → 0.1.57

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.
@@ -5,7 +5,7 @@ import {
5
5
  suiteSelectionType,
6
6
  } from "./suite-selection.mjs";
7
7
 
8
- const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
8
+ const TYPE_ORDER = ["dal", "integration", "e2e", "scenario", "load"];
9
9
 
10
10
  export function taskNeedsLocalRuntime(task) {
11
11
  return task.type !== "dal";
@@ -8,6 +8,7 @@ export function buildStatusArtifact({
8
8
  fileNames,
9
9
  shard,
10
10
  serviceFilter,
11
+ scenarioSeed,
11
12
  metadata,
12
13
  }) {
13
14
  const executedResults = results.filter((result) => !result.skipped);
@@ -68,6 +69,7 @@ export function buildStatusArtifact({
68
69
  fileNames: [...(fileNames || [])].sort(),
69
70
  shard: shard || null,
70
71
  serviceFilter: serviceFilter || null,
72
+ scenarioSeed: scenarioSeed || null,
71
73
  };
72
74
  scope.isFullRun =
73
75
  scope.types.length === 1 &&
@@ -109,6 +111,7 @@ export function buildRunArtifact({
109
111
  fileNames,
110
112
  shard,
111
113
  serviceFilter,
114
+ scenarioSeed,
112
115
  metadata,
113
116
  summarizeDbBackend,
114
117
  serviceLogs = [],
@@ -156,6 +159,7 @@ export function buildRunArtifact({
156
159
  fileNames,
157
160
  shard,
158
161
  serviceFilter,
162
+ scenarioSeed: scenarioSeed || null,
159
163
  testkitVersion: metadata.testkitVersion,
160
164
  },
161
165
  summary: {
@@ -224,6 +228,7 @@ export function buildLiveRunArtifact({
224
228
  fileNames,
225
229
  shard,
226
230
  serviceFilter,
231
+ scenarioSeed,
227
232
  metadata,
228
233
  summarizeDbBackend,
229
234
  serviceLogs = [],
@@ -244,6 +249,7 @@ export function buildLiveRunArtifact({
244
249
  fileNames,
245
250
  shard,
246
251
  serviceFilter,
252
+ scenarioSeed,
247
253
  metadata,
248
254
  summarizeDbBackend,
249
255
  serviceLogs,
@@ -62,6 +62,7 @@ describe("runner reporting", () => {
62
62
  fileNames: [],
63
63
  shard: null,
64
64
  serviceFilter: null,
65
+ scenarioSeed: "demo-seed",
65
66
  metadata: {
66
67
  git: {
67
68
  branch: "main",
@@ -84,6 +85,7 @@ describe("runner reporting", () => {
84
85
  fileTimeoutSeconds: 60,
85
86
  workerCount: 1,
86
87
  runtimeInstanceCount: 2,
88
+ scenarioSeed: "demo-seed",
87
89
  });
88
90
  expect(artifact.summary.services).toEqual({
89
91
  total: 1,
@@ -220,6 +222,7 @@ describe("runner reporting", () => {
220
222
  fileNames: ["tests/api/integration/b.int.testkit.ts"],
221
223
  shard: null,
222
224
  serviceFilter: "api",
225
+ scenarioSeed: "demo-seed",
223
226
  metadata: {
224
227
  git: {
225
228
  branch: "main",
@@ -247,6 +250,7 @@ describe("runner reporting", () => {
247
250
  fileNames: ["tests/api/integration/b.int.testkit.ts"],
248
251
  shard: null,
249
252
  serviceFilter: "api",
253
+ scenarioSeed: "demo-seed",
250
254
  isFullRun: false,
251
255
  },
252
256
  summary: {
@@ -291,6 +295,7 @@ describe("runner reporting", () => {
291
295
  fileNames: [],
292
296
  shard: null,
293
297
  serviceFilter: null,
298
+ scenarioSeed: null,
294
299
  metadata: {
295
300
  git: {
296
301
  branch: "main",
@@ -1,4 +1,4 @@
1
- const USER_TYPES = new Set(["int", "e2e", "dal", "load", "pw", "all"]);
1
+ const USER_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
2
2
 
3
3
  export function normalizeTypeValues(values = []) {
4
4
  const expanded = [];
@@ -9,7 +9,7 @@ export function normalizeTypeValues(values = []) {
9
9
  if (!value) continue;
10
10
  if (!USER_TYPES.has(value)) {
11
11
  throw new Error(
12
- `Unknown type "${value}". Expected one of: int, e2e, dal, load, pw, all.`
12
+ `Unknown type "${value}". Expected one of: int, e2e, scenario, dal, load, pw, all.`
13
13
  );
14
14
  }
15
15
  expanded.push(value);
@@ -25,7 +25,7 @@ export function normalizeTypeValues(values = []) {
25
25
  throw new Error(`"--type all" cannot be combined with other types.`);
26
26
  }
27
27
 
28
- const order = ["int", "e2e", "dal", "load", "pw", "all"];
28
+ const order = ["int", "e2e", "scenario", "dal", "load", "pw", "all"];
29
29
  return deduped.sort((left, right) => order.indexOf(left) - order.indexOf(right));
30
30
  }
31
31
 
@@ -52,7 +52,7 @@ export function parseSuiteSelectors(values = []) {
52
52
  const name = typeMatch[2].trim();
53
53
  if (!USER_TYPES.has(type) || type === "all") {
54
54
  throw new Error(
55
- `Unknown suite selector type "${type}". Expected one of: int, e2e, dal, load, pw.`
55
+ `Unknown suite selector type "${type}". Expected one of: int, e2e, scenario, dal, load, pw.`
56
56
  );
57
57
  }
58
58
  if (!name) {
@@ -11,14 +11,20 @@ import {
11
11
  describe("runner suite selection", () => {
12
12
  it("normalizes selected type values", () => {
13
13
  expect(normalizeTypeValues([])).toEqual(["all"]);
14
- expect(normalizeTypeValues(["int,e2e", "dal"])).toEqual(["int", "e2e", "dal"]);
14
+ expect(normalizeTypeValues(["int,e2e,scenario", "dal"])).toEqual([
15
+ "int",
16
+ "e2e",
17
+ "scenario",
18
+ "dal",
19
+ ]);
15
20
  expect(() => normalizeTypeValues(["all", "int"])).toThrow("cannot be combined");
16
21
  expect(() => normalizeTypeValues(["jest"])).toThrow("Unknown type");
17
22
  });
18
23
 
19
24
  it("parses suite selectors", () => {
20
- expect(parseSuiteSelectors(["auth,dal:queries"])).toEqual([
25
+ expect(parseSuiteSelectors(["auth,scenario:journeys,dal:queries"])).toEqual([
21
26
  { kind: "plain", name: "auth", raw: "auth" },
27
+ { kind: "typed", type: "scenario", name: "journeys", raw: "scenario:journeys" },
22
28
  { kind: "typed", type: "dal", name: "queries", raw: "dal:queries" },
23
29
  ]);
24
30
  expect(() => parseSuiteSelectors(["all:auth"])).toThrow("Unknown suite selector type");
@@ -36,6 +42,7 @@ describe("runner suite selection", () => {
36
42
 
37
43
  it("maps discovered suites to user-facing selection types", () => {
38
44
  expect(suiteSelectionType("integration", "k6")).toBe("int");
45
+ expect(suiteSelectionType("scenario", "k6")).toBe("scenario");
39
46
  expect(suiteSelectionType("e2e", "playwright")).toBe("pw");
40
47
  expect(suiteSelectionType("dal", "k6")).toBe("dal");
41
48
  });
@@ -2,7 +2,7 @@ import { formatError } from "./formatting.mjs";
2
2
  import { runDalTask, runHttpK6Task } from "./default-runtime-runner.mjs";
3
3
  import { runPlaywrightTask } from "./playwright-runner.mjs";
4
4
 
5
- const HTTP_K6_TYPES = new Set(["integration", "e2e", "load"]);
5
+ const HTTP_K6_TYPES = new Set(["integration", "e2e", "scenario", "load"]);
6
6
 
7
7
  export function createWorker(workerId, productDir) {
8
8
  return {
@@ -136,6 +136,15 @@ export function recordFailureDetail(detail) {
136
136
  existing.count += normalized.count;
137
137
  }
138
138
 
139
+ export function getFailureCollectionSnapshot() {
140
+ return {
141
+ phase: failureState.phase,
142
+ groupStack: [...failureState.groupStack],
143
+ failureCount: failureState.detailsByKey.size,
144
+ keys: [...failureState.detailsByKey.keys()].sort(),
145
+ };
146
+ }
147
+
139
148
  function createFailureState() {
140
149
  return {
141
150
  phase: "exec",
@@ -0,0 +1,234 @@
1
+ import { emitArtifact } from "./artifacts.js";
2
+ import { getFailureCollectionSnapshot, group } from "./checks.js";
3
+
4
+ const DEFAULT_SCENARIO_SEED = "default";
5
+
6
+ export function createScenarioRuntime(options = {}) {
7
+ const suiteSeed = normalizeSeed(options.seed);
8
+ const state = {
9
+ seed: suiteSeed,
10
+ scenarioName: null,
11
+ choices: {},
12
+ notes: {},
13
+ resources: [],
14
+ resourceState: new Map(),
15
+ steps: [],
16
+ };
17
+
18
+ return {
19
+ get seed() {
20
+ return state.seed;
21
+ },
22
+ get scenarioName() {
23
+ return state.scenarioName;
24
+ },
25
+ resource(name, factory, options = {}) {
26
+ return createScenarioResource(state, name, factory, options);
27
+ },
28
+ pick(name, choices) {
29
+ return pickChoice(state, name, choices);
30
+ },
31
+ maybe(name, probability = 0.5) {
32
+ return maybeChoice(state, name, probability);
33
+ },
34
+ choose(name, shapeOrChoices) {
35
+ return chooseScenario(state, name, shapeOrChoices);
36
+ },
37
+ note(name, value) {
38
+ const normalizedName = normalizeLabel(name, "note");
39
+ state.notes[normalizedName] = cloneForArtifact(value);
40
+ return value;
41
+ },
42
+ step(name, fn) {
43
+ return runScenarioStep(state, name, fn);
44
+ },
45
+ emitArtifact() {
46
+ emitScenarioArtifact(state);
47
+ },
48
+ snapshot() {
49
+ return buildScenarioArtifactPayload(state);
50
+ },
51
+ };
52
+ }
53
+
54
+ function createScenarioResource(state, name, factory, options = {}) {
55
+ const normalizedName = normalizeLabel(name, "resource");
56
+ if (typeof factory !== "function") {
57
+ throw new Error(`scenario.resource("${normalizedName}") requires a factory function`);
58
+ }
59
+
60
+ const scope = normalizeResourceScope(options.scope);
61
+ if (!state.resources.some((entry) => entry.name === normalizedName)) {
62
+ state.resources.push({ name: normalizedName, scope });
63
+ }
64
+
65
+ return {
66
+ get() {
67
+ if (scope === "step") {
68
+ return factory();
69
+ }
70
+
71
+ if (state.resourceState.has(normalizedName)) {
72
+ return state.resourceState.get(normalizedName);
73
+ }
74
+
75
+ const value = factory();
76
+ state.resourceState.set(normalizedName, value);
77
+ return value;
78
+ },
79
+ };
80
+ }
81
+
82
+ function pickChoice(state, name, choices) {
83
+ const normalizedName = normalizeLabel(name, "choice");
84
+ const values = Array.isArray(choices) ? choices : [];
85
+ if (values.length === 0) {
86
+ throw new Error(`scenario.pick("${normalizedName}") requires at least one choice`);
87
+ }
88
+
89
+ const index = chooseIndex(state.seed, state.scenarioName || "scenario", normalizedName, values.length);
90
+ const value = values[index];
91
+ state.choices[normalizedName] = cloneForArtifact(value);
92
+ return value;
93
+ }
94
+
95
+ function maybeChoice(state, name, probability = 0.5) {
96
+ const normalizedName = normalizeLabel(name, "choice");
97
+ const numericProbability = Number(probability);
98
+ if (!Number.isFinite(numericProbability) || numericProbability < 0 || numericProbability > 1) {
99
+ throw new Error(
100
+ `scenario.maybe("${normalizedName}") probability must be between 0 and 1`
101
+ );
102
+ }
103
+
104
+ const ratio = chooseRatio(state.seed, state.scenarioName || "scenario", normalizedName);
105
+ const value = ratio < numericProbability;
106
+ state.choices[normalizedName] = value;
107
+ return value;
108
+ }
109
+
110
+ function chooseScenario(state, name, shapeOrChoices) {
111
+ const normalizedName = normalizeLabel(name, "scenario");
112
+ state.scenarioName = normalizedName;
113
+
114
+ if (Array.isArray(shapeOrChoices)) {
115
+ const selected = pickChoice(state, `${normalizedName}:variant`, shapeOrChoices);
116
+ return selected;
117
+ }
118
+
119
+ if (!shapeOrChoices || typeof shapeOrChoices !== "object") {
120
+ throw new Error(`scenario.choose("${normalizedName}") requires an object or array`);
121
+ }
122
+
123
+ return shapeOrChoices;
124
+ }
125
+
126
+ function runScenarioStep(state, name, fn) {
127
+ const stepName = normalizeLabel(name, "unnamed step");
128
+ if (typeof fn !== "function") {
129
+ throw new Error(`scenario.step("${stepName}") requires a function`);
130
+ }
131
+
132
+ const entry = {
133
+ name: stepName,
134
+ startedAt: new Date().toISOString(),
135
+ durationMs: 0,
136
+ status: "passed",
137
+ };
138
+ state.steps.push(entry);
139
+
140
+ const startedAt = Date.now();
141
+ const before = getFailureCollectionSnapshot();
142
+
143
+ try {
144
+ return group(stepName, () => fn());
145
+ } catch (error) {
146
+ entry.status = "failed";
147
+ entry.error = error instanceof Error ? error.message : String(error);
148
+ throw error;
149
+ } finally {
150
+ const after = getFailureCollectionSnapshot();
151
+ entry.durationMs = Date.now() - startedAt;
152
+ entry.finishedAt = new Date().toISOString();
153
+ if (entry.status !== "failed" && after.failureCount > before.failureCount) {
154
+ entry.status = "failed";
155
+ entry.failureCount = after.failureCount - before.failureCount;
156
+ } else if (after.failureCount > before.failureCount) {
157
+ entry.failureCount = after.failureCount - before.failureCount;
158
+ }
159
+ }
160
+ }
161
+
162
+ function emitScenarioArtifact(state) {
163
+ const payload = buildScenarioArtifactPayload(state);
164
+ emitArtifact("scenario", payload, {
165
+ kind: "testkit.scenario",
166
+ summary: buildScenarioSummary(payload),
167
+ });
168
+ }
169
+
170
+ function buildScenarioArtifactPayload(state) {
171
+ return {
172
+ schemaVersion: 1,
173
+ seed: state.seed,
174
+ scenarioName: state.scenarioName,
175
+ choices: cloneForArtifact(state.choices),
176
+ notes: cloneForArtifact(state.notes),
177
+ resources: state.resources.map((entry) => ({ ...entry })),
178
+ steps: state.steps.map((entry) => ({ ...entry })),
179
+ };
180
+ }
181
+
182
+ function buildScenarioSummary(payload) {
183
+ const failedStep = payload.steps.find((entry) => entry.status === "failed");
184
+ if (failedStep) {
185
+ return `${payload.scenarioName || "scenario"} seed=${payload.seed} failed at ${failedStep.name}`;
186
+ }
187
+ return `${payload.scenarioName || "scenario"} seed=${payload.seed} (${payload.steps.length} steps)`;
188
+ }
189
+
190
+ function chooseIndex(seed, scenarioName, choiceName, length) {
191
+ if (length <= 1) return 0;
192
+ const value = hashString(`${seed}:${scenarioName}:${choiceName}`);
193
+ return value % length;
194
+ }
195
+
196
+ function chooseRatio(seed, scenarioName, choiceName) {
197
+ const value = hashString(`${seed}:${scenarioName}:${choiceName}`);
198
+ return value / 0xffffffff;
199
+ }
200
+
201
+ function hashString(input) {
202
+ let hash = 2166136261;
203
+ const source = String(input);
204
+ for (let index = 0; index < source.length; index += 1) {
205
+ hash ^= source.charCodeAt(index);
206
+ hash = Math.imul(hash, 16777619);
207
+ }
208
+ return hash >>> 0;
209
+ }
210
+
211
+ function normalizeSeed(value) {
212
+ if (value === undefined || value === null) return DEFAULT_SCENARIO_SEED;
213
+ const normalized = String(value).trim();
214
+ return normalized.length > 0 ? normalized : DEFAULT_SCENARIO_SEED;
215
+ }
216
+
217
+ function normalizeLabel(value, fallback) {
218
+ if (typeof value !== "string") return fallback;
219
+ const normalized = value.trim();
220
+ return normalized.length > 0 ? normalized : fallback;
221
+ }
222
+
223
+ function normalizeResourceScope(value) {
224
+ const normalized = normalizeLabel(value, "scenario");
225
+ if (normalized === "file") return "file";
226
+ if (normalized === "scenario") return "scenario";
227
+ if (normalized === "step") return "step";
228
+ throw new Error(`Unsupported scenario resource scope "${value}"`);
229
+ }
230
+
231
+ function cloneForArtifact(value) {
232
+ if (value === undefined) return null;
233
+ return JSON.parse(JSON.stringify(value));
234
+ }
@@ -0,0 +1,179 @@
1
+ import { fail } from "k6";
2
+ import {
3
+ defaultOptions,
4
+ emitFailureCollectionArtifact,
5
+ recordFailureDetail,
6
+ recordRuntimeFailure,
7
+ startFailureCollection,
8
+ } from "./checks.js";
9
+ import {
10
+ createHttpClient,
11
+ emitHttpTraceCollectionArtifact,
12
+ getEnv,
13
+ startHttpTraceCollection,
14
+ } from "./http.js";
15
+ import {
16
+ clearRuntimeContext,
17
+ registerRuntimeContext,
18
+ resolveHttpProfile,
19
+ } from "../../setup/runtime.mjs";
20
+ import { createScenarioRuntime } from "./scenario-runtime.js";
21
+
22
+ export function defineScenarioSuite(configOrRun, maybeRun) {
23
+ const { config, run } = normalizeSuiteArgs(configOrRun, maybeRun);
24
+
25
+ return {
26
+ get options() {
27
+ return mergeProfileConfig(config).options || defaultOptions;
28
+ },
29
+ setup() {
30
+ const resolved = resolveRuntimeConfig(config);
31
+ startFailureCollection("setup");
32
+ startHttpTraceCollection("setup");
33
+ try {
34
+ registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
35
+ if (typeof resolved.auth?.setup !== "function") return null;
36
+ return resolved.auth.setup({ env: resolved.env });
37
+ } catch (error) {
38
+ recordFailureDetail(buildRuntimeExceptionDetail("setup", error));
39
+ recordRuntimeFailure();
40
+ fail(formatFatalSuiteError("setup", error));
41
+ } finally {
42
+ emitFailureCollectionArtifact();
43
+ emitHttpTraceCollectionArtifact();
44
+ clearRuntimeContext();
45
+ }
46
+ },
47
+ exec(setupData) {
48
+ const resolved = resolveRuntimeConfig(config);
49
+ const scenario = createScenarioRuntime({
50
+ seed: resolved.env.rawEnv.TESTKIT_SCENARIO_SEED,
51
+ });
52
+ startFailureCollection("exec");
53
+ startHttpTraceCollection("exec");
54
+ try {
55
+ registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
56
+ return run({
57
+ env: resolved.env,
58
+ req: resolved.client.request,
59
+ rawReq: resolved.client.raw,
60
+ getWithHeaders: resolved.client.getWithHeaders,
61
+ setupData,
62
+ session: setupData,
63
+ scenario,
64
+ });
65
+ } catch (error) {
66
+ recordFailureDetail(buildRuntimeExceptionDetail("exec", error));
67
+ recordRuntimeFailure();
68
+ fail(formatFatalSuiteError("exec", error));
69
+ } finally {
70
+ scenario.emitArtifact();
71
+ emitFailureCollectionArtifact();
72
+ emitHttpTraceCollectionArtifact();
73
+ clearRuntimeContext();
74
+ }
75
+ },
76
+ };
77
+ }
78
+
79
+ function normalizeSuiteArgs(configOrRun, maybeRun) {
80
+ if (typeof configOrRun === "function") {
81
+ return { config: {}, run: configOrRun };
82
+ }
83
+ if (typeof maybeRun !== "function") {
84
+ throw new Error("suite factory requires a run callback");
85
+ }
86
+ return { config: configOrRun || {}, run: maybeRun };
87
+ }
88
+
89
+ function callHeaders(builder, setupData, env) {
90
+ if (typeof builder !== "function") return {};
91
+ return builder(setupData, { env }) || {};
92
+ }
93
+
94
+ function mergeProfileConfig(config) {
95
+ if (!config?.profile) return config || {};
96
+
97
+ const profile = resolveHttpProfile(config.profile) || {};
98
+ return {
99
+ ...profile,
100
+ ...config,
101
+ auth: config.auth ?? profile.auth ?? null,
102
+ headers: config.headers ?? profile.headers,
103
+ rawHeaders: config.rawHeaders ?? profile.rawHeaders,
104
+ options: config.options ?? profile.options,
105
+ env: config.env ?? profile.env,
106
+ };
107
+ }
108
+
109
+ function resolveRuntimeConfig(config) {
110
+ const resolvedConfig = mergeProfileConfig(config);
111
+ const env = {
112
+ ...(resolvedConfig.env || getEnv()),
113
+ rawEnv: __ENV,
114
+ };
115
+ const auth = resolvedConfig.auth || null;
116
+ const client = createHttpClient({
117
+ baseUrl: env.BASE,
118
+ routeHeaders: env.routeParams,
119
+ getHeaders(setupData) {
120
+ return {
121
+ ...callHeaders(auth?.headers, setupData, env),
122
+ ...callHeaders(resolvedConfig.headers, setupData, env),
123
+ };
124
+ },
125
+ getRawHeaders(setupData) {
126
+ return callHeaders(resolvedConfig.rawHeaders, setupData, env);
127
+ },
128
+ });
129
+
130
+ return {
131
+ resolvedConfig,
132
+ env,
133
+ auth,
134
+ client,
135
+ };
136
+ }
137
+
138
+ function formatFatalSuiteError(phase, error) {
139
+ if (error instanceof Error) {
140
+ return `Uncaught testkit suite error during ${phase}: ${error.message}`;
141
+ }
142
+ return `Uncaught testkit suite error during ${phase}: ${String(error)}`;
143
+ }
144
+
145
+ function buildRuntimeExceptionDetail(phase, error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : "";
148
+ const location = extractLocationFromStack(stack);
149
+ return {
150
+ kind: "runtime-exception",
151
+ key: location
152
+ ? `${location.path}:${location.line}:${location.column}`
153
+ : `runtime-exception:${phase}:${message}`,
154
+ title: "Uncaught runtime exception",
155
+ message: `Uncaught testkit suite error during ${phase}: ${message}`,
156
+ location,
157
+ stack,
158
+ };
159
+ }
160
+
161
+ function extractLocationFromStack(stack) {
162
+ if (!stack) return null;
163
+ const matches = [...String(stack).matchAll(/(file:\/\/[^\s)]+|\/[^\s):]+):(\d+):(\d+)/g)].map(
164
+ (match) => ({
165
+ path: normalizeStackPath(match[1]),
166
+ line: Number(match[2]),
167
+ column: Number(match[3]),
168
+ })
169
+ );
170
+ return matches[0] || null;
171
+ }
172
+
173
+ function normalizeStackPath(rawPath) {
174
+ if (typeof rawPath !== "string") return rawPath;
175
+ if (rawPath.startsWith("file://")) {
176
+ return rawPath.slice("file://".length);
177
+ }
178
+ return rawPath;
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",
@@ -17,6 +17,10 @@
17
17
  "types": "./lib/runtime/index.d.ts",
18
18
  "default": "./lib/runtime/index.mjs"
19
19
  },
20
+ "./discovery": {
21
+ "types": "./lib/discovery/index.d.ts",
22
+ "default": "./lib/discovery/index.mjs"
23
+ },
20
24
  "./known-failures": {
21
25
  "types": "./lib/known-failures/index.d.ts",
22
26
  "default": "./lib/known-failures/index.mjs"