@elench/testkit 0.1.80 → 0.1.82

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 (54) hide show
  1. package/README.md +78 -56
  2. package/lib/cli/args.mjs +2 -14
  3. package/lib/cli/args.test.mjs +1 -17
  4. package/lib/cli/command-helpers.mjs +1 -20
  5. package/lib/cli/entrypoint.mjs +0 -4
  6. package/lib/cli/presentation/colors.mjs +1 -1
  7. package/lib/cli/presentation/failure-presentation.mjs +4 -4
  8. package/lib/cli/presentation/run-reporter.mjs +23 -9
  9. package/lib/cli/presentation/run-reporter.test.mjs +12 -6
  10. package/lib/cli/presentation/summary-box.test.mjs +4 -4
  11. package/lib/cli/viewer.mjs +18 -19
  12. package/lib/config/index.mjs +6 -6
  13. package/lib/config/runtime.mjs +8 -8
  14. package/lib/config-api/auth-fixtures.mjs +762 -0
  15. package/lib/config-api/index.d.ts +96 -112
  16. package/lib/config-api/index.mjs +22 -12
  17. package/lib/config-api/index.test.mjs +61 -222
  18. package/lib/index.d.ts +29 -9
  19. package/lib/package.test.mjs +4 -4
  20. package/lib/{known-failures → regressions}/github.mjs +36 -78
  21. package/lib/regressions/github.test.mjs +324 -0
  22. package/lib/regressions/index.d.ts +189 -0
  23. package/lib/{known-failures → regressions}/index.mjs +90 -93
  24. package/lib/{known-failures → regressions}/index.test.mjs +37 -48
  25. package/lib/runner/formatting.mjs +49 -34
  26. package/lib/runner/formatting.test.mjs +16 -15
  27. package/lib/runner/metadata.mjs +1 -1
  28. package/lib/runner/orchestrator.mjs +7 -9
  29. package/lib/runner/regressions.mjs +304 -0
  30. package/lib/runner/{triage.test.mjs → regressions.test.mjs} +50 -36
  31. package/lib/runner/reporting.mjs +2 -2
  32. package/lib/runner/reporting.test.mjs +2 -2
  33. package/lib/runner/run-finalization.mjs +18 -30
  34. package/lib/runner/template-steps.mjs +2 -2
  35. package/lib/runtime/index.d.ts +50 -33
  36. package/lib/runtime/index.mjs +0 -1
  37. package/lib/runtime-src/k6/http-suite-runtime.js +147 -0
  38. package/lib/runtime-src/k6/http.js +80 -41
  39. package/lib/runtime-src/k6/scenario-suite.js +13 -110
  40. package/lib/runtime-src/k6/suite.js +13 -107
  41. package/node_modules/@elench/next-analysis/package.json +1 -1
  42. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  43. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  44. package/node_modules/@elench/ts-analysis/package.json +1 -1
  45. package/package.json +8 -8
  46. package/lib/cli/commands/known-failures/render.mjs +0 -19
  47. package/lib/cli/commands/known-failures/validate.mjs +0 -20
  48. package/lib/cli/known-failures.mjs +0 -164
  49. package/lib/config-api/profiles.mjs +0 -640
  50. package/lib/known-failures/github.test.mjs +0 -512
  51. package/lib/known-failures/index.d.ts +0 -192
  52. package/lib/runner/triage.mjs +0 -221
  53. /package/lib/{known-failures → regressions}/github-cache.mjs +0 -0
  54. /package/lib/{known-failures → regressions}/github-transport.mjs +0 -0
@@ -23,6 +23,9 @@ export interface RuntimeResponse {
23
23
 
24
24
  export interface RuntimeHttpTrace {
25
25
  id: string;
26
+ actorName: string | null;
27
+ organizationKey?: string | null;
28
+ organizationName?: string | null;
26
29
  requestId: string;
27
30
  startedAt: string;
28
31
  finishedAt?: string;
@@ -93,6 +96,27 @@ export interface RuntimeHttpClient {
93
96
  put(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
94
97
  }
95
98
 
99
+ export interface RuntimeActorRecord<TSession = Record<string, unknown>> {
100
+ actorIndex: number;
101
+ actorName: string;
102
+ email?: string;
103
+ name?: string;
104
+ organizationKey?: string;
105
+ organizationName?: string;
106
+ session: TSession | null;
107
+ }
108
+
109
+ export interface AuthSessionBundle<TSession = Record<string, unknown>> {
110
+ actors: Record<string, RuntimeActorRecord<TSession> & TSession>;
111
+ primaryActor: string | null;
112
+ }
113
+
114
+ export interface HttpHeaderBuildContext<TSession = Record<string, unknown>> {
115
+ actor: (RuntimeActorRecord<TSession> & TSession) | null;
116
+ actorName: string | null;
117
+ sessionBundle: AuthSessionBundle<TSession> | null;
118
+ }
119
+
96
120
  export interface Metric {
97
121
  add(value: number): void;
98
122
  }
@@ -109,38 +133,35 @@ export declare class Trend implements Metric {
109
133
 
110
134
  export interface HttpClientConfig<TSetup = unknown> {
111
135
  baseUrl: string;
136
+ defaultActor?: string | null;
112
137
  defaultHeaders?: RuntimeHeaders;
113
- getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void;
114
- getRawHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void;
138
+ getHeaders?: (context: HttpHeaderBuildContext<TSetup>) => RuntimeHeaders | void;
139
+ getRawHeaders?: (context: HttpHeaderBuildContext<TSetup>) => RuntimeHeaders | void;
115
140
  routeHeaders?: RuntimeHeaders;
141
+ sessionBundle?: AuthSessionBundle<TSetup> | null;
116
142
  }
117
143
 
118
- export interface HttpClient<TSetup = unknown> {
119
- delete(path: string, setupData?: TSetup | null, extraHeaders?: RuntimeHeaders): RuntimeResponse;
120
- get(path: string, setupData?: TSetup | null, extraHeaders?: RuntimeHeaders): RuntimeResponse;
121
- getWithHeaders(
122
- path: string,
123
- setupData?: TSetup | null,
124
- extraHeaders?: RuntimeHeaders
125
- ): RuntimeResponse;
126
- patch(
127
- path: string,
128
- setupData?: TSetup | null,
129
- body?: unknown,
130
- extraHeaders?: RuntimeHeaders
131
- ): RuntimeResponse;
132
- post(
133
- path: string,
134
- setupData?: TSetup | null,
135
- body?: unknown,
136
- extraHeaders?: RuntimeHeaders
137
- ): RuntimeResponse;
138
- put(
144
+ export interface ActorRequestClient {
145
+ delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
146
+ get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
147
+ patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
148
+ post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
149
+ put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
150
+ request(
151
+ method: RuntimeMethod,
139
152
  path: string,
140
- setupData?: TSetup | null,
141
153
  body?: unknown,
142
154
  extraHeaders?: RuntimeHeaders
143
155
  ): RuntimeResponse;
156
+ }
157
+
158
+ export interface HttpClient<TSetup = unknown> {
159
+ as(actorName: string): ActorRequestClient;
160
+ delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
161
+ get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
162
+ patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
163
+ post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
164
+ put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
144
165
  raw(
145
166
  method: RuntimeMethod,
146
167
  path: string,
@@ -155,7 +176,6 @@ export interface HttpClient<TSetup = unknown> {
155
176
  request(
156
177
  method: RuntimeMethod,
157
178
  path: string,
158
- setupData?: TSetup | null,
159
179
  body?: unknown,
160
180
  extraHeaders?: RuntimeHeaders
161
181
  ): RuntimeResponse;
@@ -226,19 +246,16 @@ export declare function createHttpClient<TSetup = unknown>(
226
246
  ): HttpClient<TSetup>;
227
247
  export declare function makeReq<TSetup = unknown>(
228
248
  baseUrl: string,
249
+ sessionBundle?: AuthSessionBundle<TSetup> | null,
229
250
  routeHeaders?: RuntimeHeaders,
230
- getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
231
- ): HttpClient<TSetup>["request"];
251
+ getHeaders?: (context: HttpHeaderBuildContext<TSetup>) => RuntimeHeaders | void,
252
+ defaultActor?: string | null
253
+ ): HttpClient<TSetup>;
232
254
  export declare function makeRawReq(
233
255
  baseUrl: string,
234
256
  routeHeaders?: RuntimeHeaders,
235
- getRawHeaders?: (setupData?: never) => RuntimeHeaders | void
257
+ getRawHeaders?: (context: HttpHeaderBuildContext<never>) => RuntimeHeaders | void
236
258
  ): HttpClient["raw"];
237
- export declare function makeGetWithHeaders<TSetup = unknown>(
238
- baseUrl: string,
239
- routeHeaders?: RuntimeHeaders,
240
- getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
241
- ): HttpClient<TSetup>["getWithHeaders"];
242
259
 
243
260
  export declare function expectStatus(
244
261
  response: RuntimeResponse,
@@ -48,7 +48,6 @@ export {
48
48
  defaultOptions as httpDefaultOptions,
49
49
  getEnv,
50
50
  getHttpTrace,
51
- makeGetWithHeaders,
52
51
  makeRawReq,
53
52
  makeReq,
54
53
  safeJson,
@@ -0,0 +1,147 @@
1
+ import { createHttpClient, getEnv } from "./http.js";
2
+ import { resolveHttpProfile } from "../../config-api/runtime.mjs";
3
+
4
+ export function normalizeSuiteArgs(configOrRun, maybeRun) {
5
+ if (typeof configOrRun === "function") {
6
+ return { config: {}, run: configOrRun };
7
+ }
8
+ if (typeof maybeRun !== "function") {
9
+ throw new Error("suite factory requires a run callback");
10
+ }
11
+ return { config: configOrRun || {}, run: maybeRun };
12
+ }
13
+
14
+ export function mergeProfileConfig(config) {
15
+ if (!config?.profile) return config || {};
16
+
17
+ const profile = resolveHttpProfile(config.profile) || {};
18
+ return {
19
+ ...profile,
20
+ ...config,
21
+ auth: config.auth ?? profile.auth ?? null,
22
+ headers: config.headers ?? profile.headers,
23
+ rawHeaders: config.rawHeaders ?? profile.rawHeaders,
24
+ options: config.options ?? profile.options,
25
+ env: config.env ?? profile.env,
26
+ __testkitAuthProfile: config.__testkitAuthProfile ?? profile.__testkitAuthProfile ?? null,
27
+ };
28
+ }
29
+
30
+ export function resolveRuntimeConfig(config, sessionBundle = null) {
31
+ const resolvedConfig = mergeProfileConfig(config);
32
+ const env = {
33
+ ...(resolvedConfig.env || getEnv()),
34
+ rawEnv: __ENV,
35
+ };
36
+ const auth = resolvedConfig.auth || null;
37
+ const profileMeta = resolvedConfig.__testkitAuthProfile || null;
38
+ const client = createHttpClient({
39
+ baseUrl: env.BASE,
40
+ defaultActor: profileMeta?.primaryActor || sessionBundle?.primaryActor || null,
41
+ routeHeaders: env.routeParams,
42
+ sessionBundle,
43
+ getHeaders(context) {
44
+ return {
45
+ ...callHeaders(auth?.headers, sessionBundle, env, context.actorName),
46
+ ...callHeaders(resolvedConfig.headers, sessionBundle, env, context.actorName),
47
+ };
48
+ },
49
+ getRawHeaders(context) {
50
+ return callHeaders(resolvedConfig.rawHeaders, sessionBundle, env, context.actorName);
51
+ },
52
+ });
53
+
54
+ return {
55
+ auth,
56
+ client,
57
+ env,
58
+ profileMeta,
59
+ resolvedConfig,
60
+ };
61
+ }
62
+
63
+ export function createSuiteActors(sessionBundle, client, profileMeta) {
64
+ const actorNames = profileMeta?.actorNames || Object.keys(sessionBundle?.actors || {});
65
+ const entries = actorNames.map((actorName) => {
66
+ const actorRecord = sessionBundle?.actors?.[actorName] || null;
67
+ return {
68
+ email: actorRecord?.email || null,
69
+ index: Number(actorRecord?.actorIndex ?? 0),
70
+ key: actorName,
71
+ name: actorRecord?.name || null,
72
+ organizationKey: actorRecord?.organizationKey || null,
73
+ organizationName: actorRecord?.organizationName || null,
74
+ req: client.as(actorName),
75
+ session: actorRecord?.session || null,
76
+ };
77
+ });
78
+ const byName = new Map(entries.map((entry, index) => [actorNames[index], entry]));
79
+ const primaryName = profileMeta?.primaryActor || sessionBundle?.primaryActor || null;
80
+ const primary = primaryName ? byName.get(primaryName) || null : null;
81
+
82
+ return {
83
+ actor: primary,
84
+ actors: {
85
+ names: actorNames,
86
+ primary,
87
+ get(name) {
88
+ const actor = byName.get(name);
89
+ if (!actor) {
90
+ throw new Error(`Unknown testkit actor "${name}"`);
91
+ }
92
+ return actor;
93
+ },
94
+ has(name) {
95
+ return byName.has(name);
96
+ },
97
+ list() {
98
+ return entries.slice();
99
+ },
100
+ },
101
+ };
102
+ }
103
+
104
+ export function formatFatalSuiteError(phase, error) {
105
+ if (error instanceof Error) {
106
+ return `Uncaught testkit suite error during ${phase}: ${error.message}`;
107
+ }
108
+ return `Uncaught testkit suite error during ${phase}: ${String(error)}`;
109
+ }
110
+
111
+ export function buildRuntimeExceptionDetail(phase, error) {
112
+ const message =
113
+ error instanceof Error ? error.message : String(error);
114
+ const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : "";
115
+ const location = extractLocationFromStack(stack);
116
+ return {
117
+ kind: "runtime-exception",
118
+ key: location ? `${location.path}:${location.line}:${location.column}` : `runtime-exception:${phase}:${message}`,
119
+ title: "Uncaught runtime exception",
120
+ message: `Uncaught testkit suite error during ${phase}: ${message}`,
121
+ location,
122
+ stack,
123
+ };
124
+ }
125
+
126
+ function callHeaders(builder, sessionBundle, env, actorName) {
127
+ if (typeof builder !== "function") return {};
128
+ return builder(sessionBundle, { actor: actorName || null, env }) || {};
129
+ }
130
+
131
+ function extractLocationFromStack(stack) {
132
+ if (!stack) return null;
133
+ const matches = [...String(stack).matchAll(/(file:\/\/[^\s)]+|\/[^\s):]+):(\d+):(\d+)/g)].map((match) => ({
134
+ path: normalizeStackPath(match[1]),
135
+ line: Number(match[2]),
136
+ column: Number(match[3]),
137
+ }));
138
+ return matches[0] || null;
139
+ }
140
+
141
+ function normalizeStackPath(rawPath) {
142
+ if (typeof rawPath !== "string") return rawPath;
143
+ if (rawPath.startsWith("file://")) {
144
+ return rawPath.slice("file://".length);
145
+ }
146
+ return rawPath;
147
+ }
@@ -24,8 +24,10 @@ export function getEnv() {
24
24
  export function createHttpClient(config) {
25
25
  const {
26
26
  baseUrl,
27
+ defaultActor = null,
27
28
  routeHeaders = {},
28
29
  defaultHeaders = { "Content-Type": "application/json" },
30
+ sessionBundle = null,
29
31
  getHeaders = null,
30
32
  getRawHeaders = null,
31
33
  } = config;
@@ -34,55 +36,92 @@ export function createHttpClient(config) {
34
36
  throw new Error("baseUrl is required");
35
37
  }
36
38
 
37
- function buildHeaders(builder, setupData, extraHeaders = {}) {
39
+ function buildHeaders(builder, context, extraHeaders = {}) {
38
40
  return {
39
41
  ...defaultHeaders,
40
- ...safeHeaders(builder, setupData),
42
+ ...safeHeaders(builder, context),
41
43
  ...routeHeaders,
42
44
  ...extraHeaders,
43
45
  };
44
46
  }
45
47
 
46
- function request(method, path, setupData, body, extraHeaders = {}) {
48
+ function buildHeaderContext(actorName = null) {
49
+ return {
50
+ actor: actorName && sessionBundle?.actors ? sessionBundle.actors[actorName] || null : null,
51
+ actorName,
52
+ sessionBundle,
53
+ };
54
+ }
55
+
56
+ function ensureKnownActor(actorName) {
57
+ if (!actorName || !sessionBundle?.actors) return;
58
+ if (!sessionBundle.actors[actorName]) {
59
+ throw new Error(`Unknown testkit actor "${actorName}"`);
60
+ }
61
+ }
62
+
63
+ function requestAs(actorName, method, path, body, extraHeaders = {}) {
64
+ ensureKnownActor(actorName);
47
65
  const url = `${baseUrl}${path}`;
48
- const headers = buildHeaders(getHeaders, setupData, extraHeaders);
49
- return runHttpRequest(method, path, url, body, headers);
66
+ const headerContext = buildHeaderContext(actorName);
67
+ const headers = buildHeaders(getHeaders, headerContext, extraHeaders);
68
+ return runHttpRequest(method, path, url, body, headers, headerContext.actor);
50
69
  }
51
70
 
52
71
  function raw(method, path, body, extraHeaders = {}) {
53
72
  const url = `${baseUrl}${path}`;
54
- const headers = buildHeaders(getRawHeaders, null, extraHeaders);
55
- return runHttpRequest(method, path, url, body, headers);
73
+ const headerContext = buildHeaderContext(null);
74
+ const headers = buildHeaders(getRawHeaders, headerContext, extraHeaders);
75
+ return runHttpRequest(method, path, url, body, headers, null);
56
76
  }
57
77
 
58
- function getWithHeaders(path, setupData, extraHeaders = {}) {
59
- return runHttpRequest(
60
- "GET",
61
- path,
62
- `${baseUrl}${path}`,
63
- null,
64
- buildHeaders(getHeaders, setupData, extraHeaders)
65
- );
78
+ function createActorClient(actorName) {
79
+ return {
80
+ delete(path, extraHeaders = {}) {
81
+ return requestAs(actorName, "DELETE", path, null, extraHeaders);
82
+ },
83
+ get(path, extraHeaders = {}) {
84
+ return requestAs(actorName, "GET", path, null, extraHeaders);
85
+ },
86
+ patch(path, body, extraHeaders = {}) {
87
+ return requestAs(actorName, "PATCH", path, body, extraHeaders);
88
+ },
89
+ post(path, body, extraHeaders = {}) {
90
+ return requestAs(actorName, "POST", path, body, extraHeaders);
91
+ },
92
+ put(path, body, extraHeaders = {}) {
93
+ return requestAs(actorName, "PUT", path, body, extraHeaders);
94
+ },
95
+ request(method, path, body, extraHeaders = {}) {
96
+ return requestAs(actorName, method, path, body, extraHeaders);
97
+ },
98
+ };
66
99
  }
67
100
 
101
+ const defaultClient = createActorClient(defaultActor);
102
+
68
103
  return {
69
104
  rawHttp: http,
70
- request,
105
+ as(actorName) {
106
+ ensureKnownActor(actorName);
107
+ return createActorClient(actorName);
108
+ },
109
+ request: defaultClient.request,
71
110
  raw,
72
- get(path, setupData, extraHeaders = {}) {
73
- return request("GET", path, setupData, null, extraHeaders);
111
+ get(path, extraHeaders = {}) {
112
+ return defaultClient.get(path, extraHeaders);
74
113
  },
75
- put(path, setupData, body, extraHeaders = {}) {
76
- return request("PUT", path, setupData, body, extraHeaders);
114
+ put(path, body, extraHeaders = {}) {
115
+ return defaultClient.put(path, body, extraHeaders);
77
116
  },
78
- post(path, setupData, body, extraHeaders = {}) {
79
- return request("POST", path, setupData, body, extraHeaders);
117
+ post(path, body, extraHeaders = {}) {
118
+ return defaultClient.post(path, body, extraHeaders);
80
119
  },
81
- patch(path, setupData, body, extraHeaders = {}) {
82
- return request("PATCH", path, setupData, body, extraHeaders);
120
+ patch(path, body, extraHeaders = {}) {
121
+ return defaultClient.patch(path, body, extraHeaders);
83
122
  },
84
- delete(path, setupData, extraHeaders = {}) {
85
- return request("DELETE", path, setupData, null, extraHeaders);
123
+ delete(path, extraHeaders = {}) {
124
+ return defaultClient.delete(path, extraHeaders);
86
125
  },
87
126
  rawGet(path, extraHeaders = {}) {
88
127
  return raw("GET", path, null, extraHeaders);
@@ -99,16 +138,17 @@ export function createHttpClient(config) {
99
138
  rawDelete(path, extraHeaders = {}) {
100
139
  return raw("DELETE", path, null, extraHeaders);
101
140
  },
102
- getWithHeaders,
103
141
  };
104
142
  }
105
143
 
106
- export function makeReq(baseUrl, routeHeaders = {}, getHeaders = null) {
144
+ export function makeReq(baseUrl, sessionBundle = null, routeHeaders = {}, getHeaders = null, defaultActor = null) {
107
145
  return createHttpClient({
108
146
  baseUrl,
109
147
  routeHeaders,
148
+ sessionBundle,
149
+ defaultActor,
110
150
  getHeaders,
111
- }).request;
151
+ });
112
152
  }
113
153
 
114
154
  export function makeRawReq(baseUrl, routeHeaders = {}, getRawHeaders = null) {
@@ -119,15 +159,7 @@ export function makeRawReq(baseUrl, routeHeaders = {}, getRawHeaders = null) {
119
159
  }).raw;
120
160
  }
121
161
 
122
- export function makeGetWithHeaders(baseUrl, routeHeaders = {}, getHeaders = null) {
123
- return createHttpClient({
124
- baseUrl,
125
- routeHeaders,
126
- getHeaders,
127
- }).getWithHeaders;
128
- }
129
-
130
- function runHttpRequest(method, path, url, body, headers) {
162
+ function runHttpRequest(method, path, url, body, headers, actorRecord = null) {
131
163
  const ordinal = nextTraceOrdinal();
132
164
  const requestId = buildRequestId(ordinal);
133
165
  const finalHeaders = {
@@ -140,6 +172,7 @@ function runHttpRequest(method, path, url, body, headers) {
140
172
  method,
141
173
  path,
142
174
  url,
175
+ actorRecord,
143
176
  requestHeaders: finalHeaders,
144
177
  });
145
178
  const options = { headers: finalHeaders };
@@ -190,7 +223,10 @@ export function summarizeHttpTrace(response) {
190
223
  const trace = getHttpTrace(response);
191
224
  if (!trace) return null;
192
225
  return {
226
+ actorName: trace.actorName,
193
227
  id: trace.id,
228
+ organizationKey: trace.organizationKey,
229
+ organizationName: trace.organizationName,
194
230
  requestId: trace.requestId,
195
231
  method: trace.method,
196
232
  path: trace.path,
@@ -235,9 +271,12 @@ export function toBodyPreview(response) {
235
271
  return truncate(rawBody, TRACE_PREVIEW_LIMIT);
236
272
  }
237
273
 
238
- function createTrace({ ordinal, requestId, method, path, url, requestHeaders }) {
274
+ function createTrace({ ordinal, requestId, method, path, url, actorRecord, requestHeaders }) {
239
275
  return {
276
+ actorName: actorRecord?.actorName || null,
240
277
  id: `${traceState.phase}-${String(ordinal).padStart(3, "0")}`,
278
+ organizationKey: actorRecord?.organizationKey || null,
279
+ organizationName: actorRecord?.organizationName || null,
241
280
  requestId,
242
281
  startedAt: new Date().toISOString(),
243
282
  method,
@@ -342,9 +381,9 @@ function normalizeLabel(value, fallback) {
342
381
  return normalized.length > 0 ? normalized : fallback;
343
382
  }
344
383
 
345
- function safeHeaders(builder, setupData) {
384
+ function safeHeaders(builder, context) {
346
385
  if (typeof builder !== "function") return {};
347
- return builder(setupData) || {};
386
+ return builder(context) || {};
348
387
  }
349
388
 
350
389
  function createWrappedResponse(rawResponse, trace) {
@@ -7,17 +7,22 @@ import {
7
7
  startFailureCollection,
8
8
  } from "./checks.js";
9
9
  import {
10
- createHttpClient,
11
10
  emitHttpTraceCollectionArtifact,
12
- getEnv,
13
11
  startHttpTraceCollection,
14
12
  } from "./http.js";
15
13
  import {
16
14
  clearRuntimeContext,
17
15
  registerRuntimeContext,
18
- resolveHttpProfile,
19
16
  } from "../../config-api/runtime.mjs";
20
17
  import { createScenarioRuntime } from "./scenario-runtime.js";
18
+ import {
19
+ buildRuntimeExceptionDetail,
20
+ createSuiteActors,
21
+ formatFatalSuiteError,
22
+ mergeProfileConfig,
23
+ normalizeSuiteArgs,
24
+ resolveRuntimeConfig,
25
+ } from "./http-suite-runtime.js";
21
26
 
22
27
  export function defineScenarioSuite(configOrRun, maybeRun) {
23
28
  const { config, run } = normalizeSuiteArgs(configOrRun, maybeRun);
@@ -45,7 +50,8 @@ export function defineScenarioSuite(configOrRun, maybeRun) {
45
50
  }
46
51
  },
47
52
  exec(setupData) {
48
- const resolved = resolveRuntimeConfig(config);
53
+ const resolved = resolveRuntimeConfig(config, setupData);
54
+ const actorContext = createSuiteActors(setupData, resolved.client, resolved.profileMeta);
49
55
  const scenario = createScenarioRuntime({
50
56
  seed: resolved.env.rawEnv.TESTKIT_SCENARIO_SEED,
51
57
  });
@@ -54,12 +60,11 @@ export function defineScenarioSuite(configOrRun, maybeRun) {
54
60
  try {
55
61
  registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
56
62
  return run({
63
+ actor: actorContext.actor,
64
+ actors: actorContext.actors,
57
65
  env: resolved.env,
58
- req: resolved.client.request,
66
+ req: resolved.client,
59
67
  rawReq: resolved.client.raw,
60
- getWithHeaders: resolved.client.getWithHeaders,
61
- setupData,
62
- session: setupData,
63
68
  scenario,
64
69
  });
65
70
  } catch (error) {
@@ -75,105 +80,3 @@ export function defineScenarioSuite(configOrRun, maybeRun) {
75
80
  },
76
81
  };
77
82
  }
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
- }