@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
@@ -1,640 +0,0 @@
1
- import { runtimeHttp, runtimeJson } from "./runtime.mjs";
2
-
3
- export function customProfile(profile) {
4
- return profile || {};
5
- }
6
-
7
- export function rawProfile(options = {}) {
8
- const headerBuilder = createCompositeHeaderBuilder({
9
- defaultActor: null,
10
- headers: options.headers,
11
- resolveSessionData() {
12
- return null;
13
- },
14
- });
15
-
16
- return customProfile({
17
- env: options.env,
18
- options: options.options,
19
- auth: null,
20
- headers: (_setupData, context) => headerBuilder(null, context?.env),
21
- rawHeaders: (_setupData, context) => headerBuilder(null, context?.env),
22
- });
23
- }
24
-
25
- export function sessionProfile(options = {}) {
26
- const actor = normalizeActorConfig(options.actor, "profiles.session actor");
27
- const profileHeaders = options.headers || {};
28
- const headerBuilder = createCompositeHeaderBuilder({
29
- defaultActor: "primary",
30
- headers: profileHeaders,
31
- resolveSessionData(setupData) {
32
- return setupData || null;
33
- },
34
- });
35
-
36
- return customProfile({
37
- env: options.env,
38
- options: options.options,
39
- auth: {
40
- setup({ env }) {
41
- return resolveActorSession({
42
- actorName: "primary",
43
- actorConfig: actor,
44
- env,
45
- });
46
- },
47
- headers(setupData) {
48
- return buildAuthHeaders({
49
- sessionData: setupData || null,
50
- auth: actor.session?.auth,
51
- });
52
- },
53
- },
54
- headers(setupData, context) {
55
- return headerBuilder(setupData || null, context?.env);
56
- },
57
- rawHeaders(setupData, context) {
58
- return headerBuilder(setupData || null, context?.env);
59
- },
60
- });
61
- }
62
-
63
- export function multiActorProfile(options = {}) {
64
- const normalizedActors = normalizeActorMap(options.actors);
65
- const actorNames = Object.keys(normalizedActors);
66
- if (actorNames.length === 0) {
67
- throw new Error("profiles.multiActor(...) requires at least one actor");
68
- }
69
- const primaryActor = options.primaryActor || actorNames[0];
70
- if (!normalizedActors[primaryActor]) {
71
- throw new Error(`profiles.multiActor(...) primaryActor "${primaryActor}" is not defined`);
72
- }
73
-
74
- const headerBuilder = createCompositeHeaderBuilder({
75
- defaultActor: primaryActor,
76
- headers: options.headers || {},
77
- resolveSessionData(setupData, entry) {
78
- const targetActor = entry?.actor || primaryActor;
79
- return setupData?.[targetActor] || null;
80
- },
81
- });
82
-
83
- return customProfile({
84
- env: options.env,
85
- options: options.options,
86
- auth: {
87
- setup({ env }) {
88
- const setupData = {};
89
- actorNames.forEach((actorName, index) => {
90
- setupData[actorName] = resolveActorSession({
91
- actorName,
92
- actorIndex: index,
93
- actorConfig: normalizedActors[actorName],
94
- env,
95
- });
96
- });
97
- return setupData;
98
- },
99
- headers(setupData) {
100
- return buildAuthHeaders({
101
- sessionData: setupData?.[primaryActor] || null,
102
- auth: normalizedActors[primaryActor].session?.auth,
103
- });
104
- },
105
- },
106
- headers(setupData, context) {
107
- return headerBuilder(setupData || {}, context?.env);
108
- },
109
- rawHeaders(setupData, context) {
110
- return headerBuilder(setupData || {}, context?.env);
111
- },
112
- });
113
- }
114
-
115
- export function localJsonProfile(options = {}) {
116
- const baseOptions = normalizeLocalJsonBaseOptions(options);
117
-
118
- return {
119
- raw(overrides = {}) {
120
- const mergedHeaders = mergeLocalJsonHeaderOptions(baseOptions.headers, overrides.headers);
121
- return rawProfile({
122
- env: overrides.env ?? baseOptions.env,
123
- options: overrides.options ?? baseOptions.options,
124
- headers: buildLocalJsonHeaderConfig(mergedHeaders),
125
- });
126
- },
127
- session(overrides = {}) {
128
- const actorName = overrides.actor || "primary";
129
- const actorConfig = createLocalJsonActorConfig({
130
- actorName,
131
- baseOptions,
132
- identityOverride: overrides.identity,
133
- });
134
- const mergedHeaders = mergeLocalJsonHeaderOptions(baseOptions.headers, overrides.headers);
135
- return sessionProfile({
136
- env: overrides.env ?? baseOptions.env,
137
- options: overrides.options ?? baseOptions.options,
138
- actor: actorConfig,
139
- headers: buildLocalJsonHeaderConfig(mergedHeaders),
140
- });
141
- },
142
- multiActor(overrides = {}) {
143
- const actorEntries = resolveLocalJsonActorEntries({
144
- actors: overrides.actors,
145
- identities: baseOptions.identities,
146
- });
147
- if (actorEntries.length === 0) {
148
- throw new Error("profiles.localJson(...).multiActor(...) requires at least one actor");
149
- }
150
- const primaryActor = overrides.primaryActor || actorEntries[0].actorName;
151
- const actorMap = {};
152
- actorEntries.forEach(({ actorName, identity }) => {
153
- actorMap[actorName] = createLocalJsonActorConfig({
154
- actorName,
155
- baseOptions,
156
- identityOverride: identity,
157
- });
158
- });
159
- const mergedHeaders = mergeLocalJsonHeaderOptions(baseOptions.headers, overrides.headers);
160
- return multiActorProfile({
161
- env: overrides.env ?? baseOptions.env,
162
- options: overrides.options ?? baseOptions.options,
163
- primaryActor,
164
- actors: actorMap,
165
- headers: buildLocalJsonHeaderConfig(mergedHeaders),
166
- });
167
- },
168
- };
169
- }
170
-
171
- function normalizeActorConfig(actor, label) {
172
- if (!actor || typeof actor !== "object" || Array.isArray(actor)) {
173
- throw new Error(`${label} must be an object`);
174
- }
175
- if (!actor.login || typeof actor.login !== "object" || Array.isArray(actor.login)) {
176
- throw new Error(`${label}.login is required`);
177
- }
178
- if (!actor.session || typeof actor.session !== "object" || Array.isArray(actor.session)) {
179
- throw new Error(`${label}.session is required`);
180
- }
181
-
182
- return {
183
- bootstrap: normalizeRequestList(actor.bootstrap),
184
- login: actor.login,
185
- session: actor.session,
186
- };
187
- }
188
-
189
- function normalizeActorMap(actors) {
190
- if (!actors || typeof actors !== "object" || Array.isArray(actors)) {
191
- throw new Error("profiles.multiActor(...) actors must be an object");
192
- }
193
- const normalized = {};
194
- for (const [actorName, actorConfig] of Object.entries(actors)) {
195
- normalized[actorName] = normalizeActorConfig(actorConfig, `profiles.multiActor actors.${actorName}`);
196
- }
197
- return normalized;
198
- }
199
-
200
- function normalizeRequestList(value) {
201
- if (value == null) return [];
202
- return Array.isArray(value) ? [...value] : [value];
203
- }
204
-
205
- function normalizeLocalJsonBaseOptions(options) {
206
- return {
207
- env: options.env,
208
- headers: normalizeLocalJsonHeaderOptions(options.headers),
209
- identities: options.identities && typeof options.identities === "object" && !Array.isArray(options.identities)
210
- ? { ...options.identities }
211
- : {},
212
- login: normalizeLocalJsonLoginOptions(options.login),
213
- options: options.options,
214
- password: options.password,
215
- session: normalizeLocalJsonSessionOptions(options.session),
216
- signup: normalizeLocalJsonSignupOptions(options.signup),
217
- };
218
- }
219
-
220
- function normalizeLocalJsonSignupOptions(options) {
221
- if (options === false) {
222
- return { enabled: false };
223
- }
224
-
225
- const normalized = options && typeof options === "object" && !Array.isArray(options) ? { ...options } : {};
226
- return {
227
- enabled: normalized.enabled !== false,
228
- expect: normalized.expect ?? [201, 409],
229
- path: normalized.path || "/api/v1/auth/signup",
230
- };
231
- }
232
-
233
- function normalizeLocalJsonLoginOptions(options) {
234
- const normalized = options && typeof options === "object" && !Array.isArray(options) ? { ...options } : {};
235
- return {
236
- expect: normalized.expect ?? 200,
237
- path: normalized.path || "/api/v1/auth/login",
238
- };
239
- }
240
-
241
- function normalizeLocalJsonSessionOptions(options) {
242
- const normalized = options && typeof options === "object" && !Array.isArray(options) ? { ...options } : {};
243
- return {
244
- auth: normalized.auth && typeof normalized.auth === "object" && !Array.isArray(normalized.auth)
245
- ? { ...normalized.auth }
246
- : {},
247
- authCookie: normalized.authCookie || null,
248
- cookies: normalized.cookies && typeof normalized.cookies === "object" && !Array.isArray(normalized.cookies)
249
- ? { ...normalized.cookies }
250
- : {},
251
- fields: normalized.fields && typeof normalized.fields === "object" && !Array.isArray(normalized.fields)
252
- ? { ...normalized.fields }
253
- : {},
254
- organizationIdPath: normalized.organizationIdPath || null,
255
- refreshCookie: normalized.refreshCookie || null,
256
- };
257
- }
258
-
259
- function normalizeLocalJsonHeaderOptions(options) {
260
- const normalized = options && typeof options === "object" && !Array.isArray(options) ? { ...options } : {};
261
- return {
262
- contentTypeJson: normalized.contentTypeJson,
263
- forwardedFor: normalized.forwardedFor,
264
- organization: normalized.organization === undefined ? "X-Organization-Id" : normalized.organization,
265
- values: normalized.values,
266
- };
267
- }
268
-
269
- function mergeLocalJsonHeaderOptions(baseOptions, overrideOptions) {
270
- const normalizedBase = normalizeLocalJsonHeaderOptions(baseOptions);
271
- const normalizedOverride = normalizeLocalJsonHeaderOptions(overrideOptions);
272
- return {
273
- contentTypeJson:
274
- normalizedOverride.contentTypeJson !== undefined
275
- ? normalizedOverride.contentTypeJson
276
- : normalizedBase.contentTypeJson,
277
- forwardedFor:
278
- normalizedOverride.forwardedFor !== undefined
279
- ? normalizedOverride.forwardedFor
280
- : normalizedBase.forwardedFor,
281
- organization:
282
- normalizedOverride.organization !== undefined
283
- ? normalizedOverride.organization
284
- : normalizedBase.organization,
285
- values: normalizedOverride.values !== undefined ? normalizedOverride.values : normalizedBase.values,
286
- };
287
- }
288
-
289
- function resolveLocalJsonActorEntries({ actors, identities }) {
290
- if (Array.isArray(actors)) {
291
- return actors.map((actorName) => ({
292
- actorName,
293
- identity: identities?.[actorName],
294
- }));
295
- }
296
- if (actors && typeof actors === "object") {
297
- return Object.entries(actors).map(([actorName, identity]) => ({
298
- actorName,
299
- identity,
300
- }));
301
- }
302
-
303
- const identityEntries = Object.entries(identities || {}).filter(([actorName]) => actorName !== "*");
304
- return identityEntries.map(([actorName, identity]) => ({ actorName, identity }));
305
- }
306
-
307
- function createLocalJsonActorConfig({ actorName, baseOptions, identityOverride }) {
308
- const identity = resolveLocalJsonIdentity({
309
- actorName,
310
- identities: baseOptions.identities,
311
- identityOverride,
312
- });
313
- const password = resolveLocalJsonIdentityValue(identity.password ?? baseOptions.password, actorName, `password for actor "${actorName}"`);
314
- const email = resolveLocalJsonIdentityValue(identity.email, actorName, `email for actor "${actorName}"`);
315
- const name = resolveLocalJsonIdentityOptionalValue(identity.name, actorName);
316
- const organizationName = resolveLocalJsonIdentityOptionalValue(identity.organizationName, actorName);
317
-
318
- const signupBody = {
319
- email,
320
- password,
321
- ...(name ? { name } : {}),
322
- ...(organizationName ? { organizationName } : {}),
323
- ...normalizeObject(identity.signupBody),
324
- };
325
- const loginBody = {
326
- email,
327
- password,
328
- ...normalizeObject(identity.loginBody),
329
- };
330
-
331
- return {
332
- bootstrap: baseOptions.signup.enabled
333
- ? {
334
- path: baseOptions.signup.path,
335
- expect: baseOptions.signup.expect,
336
- body: () => signupBody,
337
- }
338
- : [],
339
- login: {
340
- path: baseOptions.login.path,
341
- expect: baseOptions.login.expect,
342
- body: () => loginBody,
343
- },
344
- session: buildLocalJsonSessionCaptureConfig(baseOptions.session),
345
- };
346
- }
347
-
348
- function resolveLocalJsonIdentity({ actorName, identities, identityOverride }) {
349
- const baseIdentity = normalizeObject(identities?.["*"]);
350
- const namedIdentity = normalizeObject(identities?.[actorName]);
351
- return {
352
- ...baseIdentity,
353
- ...namedIdentity,
354
- ...normalizeObject(identityOverride),
355
- };
356
- }
357
-
358
- function resolveLocalJsonIdentityValue(value, actorName, label) {
359
- const resolved = resolveLocalJsonIdentityOptionalValue(value, actorName);
360
- if (resolved == null || resolved === "") {
361
- throw new Error(`profiles.localJson(...) requires ${label}`);
362
- }
363
- return resolved;
364
- }
365
-
366
- function resolveLocalJsonIdentityOptionalValue(value, actorName) {
367
- if (typeof value === "function") {
368
- return value({ actor: actorName });
369
- }
370
- if (typeof value !== "string") {
371
- return value ?? null;
372
- }
373
- return value.replaceAll("{actor}", actorName);
374
- }
375
-
376
- function normalizeObject(value) {
377
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
378
- }
379
-
380
- function buildLocalJsonSessionCaptureConfig(sessionOptions) {
381
- const cookies = {
382
- ...(sessionOptions.authCookie
383
- ? { [(sessionOptions.auth?.sourceKey || "jwt")]: sessionOptions.authCookie }
384
- : {}),
385
- ...(sessionOptions.refreshCookie ? { refreshToken: sessionOptions.refreshCookie } : {}),
386
- ...sessionOptions.cookies,
387
- };
388
- const fields = {
389
- ...(sessionOptions.organizationIdPath ? { organizationId: sessionOptions.organizationIdPath } : {}),
390
- ...sessionOptions.fields,
391
- };
392
-
393
- return {
394
- cookies,
395
- fields,
396
- ...(sessionOptions.authCookie || sessionOptions.auth?.sourceKey
397
- ? {
398
- auth: {
399
- header: sessionOptions.auth?.header,
400
- prefix: sessionOptions.auth?.prefix,
401
- source: {
402
- key: sessionOptions.auth?.sourceKey || "jwt",
403
- },
404
- },
405
- }
406
- : {}),
407
- };
408
- }
409
-
410
- function buildLocalJsonHeaderConfig(headerOptions) {
411
- const fromSession = [];
412
- if (headerOptions.organization !== false) {
413
- if (typeof headerOptions.organization === "string") {
414
- fromSession.push({
415
- header: headerOptions.organization,
416
- field: "organizationId",
417
- });
418
- } else if (headerOptions.organization && typeof headerOptions.organization === "object") {
419
- fromSession.push({
420
- actor: headerOptions.organization.actor,
421
- field: headerOptions.organization.field || "organizationId",
422
- header: headerOptions.organization.header || "X-Organization-Id",
423
- });
424
- }
425
- }
426
-
427
- return {
428
- ...(headerOptions.contentTypeJson !== undefined ? { contentTypeJson: headerOptions.contentTypeJson } : {}),
429
- ...(headerOptions.forwardedFor !== undefined ? { forwardedFor: headerOptions.forwardedFor } : {}),
430
- ...(fromSession.length > 0 ? { fromSession } : {}),
431
- ...(headerOptions.values !== undefined ? { values: headerOptions.values } : {}),
432
- };
433
- }
434
-
435
- function resolveActorSession({ actorName, actorConfig, env, actorIndex = 0 }) {
436
- const requestContext = { actor: actorName, actorIndex, env };
437
- actorConfig.bootstrap.forEach((requestConfig, index) => {
438
- runProfileRequest({
439
- requestConfig,
440
- context: { ...requestContext, phase: `bootstrap:${index}` },
441
- label: `profiles bootstrap request for actor "${actorName}"`,
442
- });
443
- });
444
-
445
- const loginResponse = runProfileRequest({
446
- requestConfig: actorConfig.login,
447
- context: { ...requestContext, phase: "login" },
448
- label: `profiles login request for actor "${actorName}"`,
449
- });
450
-
451
- return extractSessionData(loginResponse, actorConfig.session);
452
- }
453
-
454
- function runProfileRequest({ requestConfig, context, label }) {
455
- const method = normalizeMethod(requestConfig.method);
456
- const path = requestConfig.path;
457
- if (typeof path !== "string" || path.trim().length === 0) {
458
- throw new Error(`${label} requires a non-empty path`);
459
- }
460
-
461
- const bodyValue = resolveFactoryValue(requestConfig.body, context);
462
- const headersValue = resolveFactoryValue(requestConfig.headers, context) || {};
463
- const headers = {
464
- ...(requestConfig.contentTypeJson === false ? {} : { "Content-Type": "application/json" }),
465
- ...(context.env?.routeParams || {}),
466
- ...headersValue,
467
- };
468
- const expectedStatuses = normalizeExpectedStatuses(requestConfig.expect);
469
- const url = `${context.env.BASE}${path}`;
470
- const response = dispatchProfileRequest(method, url, bodyValue, headers);
471
-
472
- if (!expectedStatuses.includes(Number(response?.status))) {
473
- throw new Error(
474
- `${label} failed (${response?.status ?? "unknown"}): ${response?.body ?? "no response body"}`
475
- );
476
- }
477
-
478
- return response;
479
- }
480
-
481
- function dispatchProfileRequest(method, url, body, headers) {
482
- if (method === "GET") return runtimeHttp.get(url, { headers });
483
- if (method === "DELETE") return runtimeHttp.del(url, body == null ? null : JSON.stringify(body), { headers });
484
- if (method === "PUT") return runtimeHttp.put(url, JSON.stringify(body ?? null), { headers });
485
- if (method === "PATCH") return runtimeHttp.patch(url, JSON.stringify(body ?? null), { headers });
486
- return runtimeHttp.post(url, JSON.stringify(body ?? null), { headers });
487
- }
488
-
489
- function normalizeMethod(value) {
490
- const method = String(value || "POST").trim().toUpperCase();
491
- if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
492
- throw new Error(`Unsupported profile request method "${method}"`);
493
- }
494
- return method;
495
- }
496
-
497
- function normalizeExpectedStatuses(value) {
498
- if (value == null) return [200];
499
- return Array.isArray(value) ? value.map((entry) => Number(entry)) : [Number(value)];
500
- }
501
-
502
- function resolveFactoryValue(value, context) {
503
- if (typeof value === "function") {
504
- return value(context);
505
- }
506
- return value;
507
- }
508
-
509
- function extractSessionData(response, sessionConfig) {
510
- const data = {};
511
- const body = safeRuntimeJson(response);
512
-
513
- for (const [fieldName, cookieName] of Object.entries(sessionConfig.cookies || {})) {
514
- data[fieldName] = extractCookieValue(response, cookieName);
515
- }
516
-
517
- for (const [fieldName, fieldPath] of Object.entries(sessionConfig.fields || {})) {
518
- data[fieldName] = readValueAtPath(body, fieldPath);
519
- }
520
-
521
- return data;
522
- }
523
-
524
- function buildAuthHeaders({ sessionData, auth }) {
525
- if (!auth) return {};
526
- const source = auth.source || {};
527
- const value = sessionData?.[source.key];
528
- if (!value) return {};
529
-
530
- const header = auth.header || "Authorization";
531
- const prefix = auth.prefix === undefined ? "Bearer " : String(auth.prefix);
532
- return {
533
- [header]: `${prefix}${value}`,
534
- };
535
- }
536
-
537
- function createCompositeHeaderBuilder({ defaultActor, headers, resolveSessionData }) {
538
- const normalized = headers || {};
539
-
540
- return function buildHeaders(setupData, env) {
541
- const actorContext = normalized.forwardedFor && typeof normalized.forwardedFor === "object"
542
- ? normalized.forwardedFor.actor || defaultActor || null
543
- : defaultActor || null;
544
- const sessionDataForValues = resolveSessionData(setupData, { actor: actorContext });
545
- const valueContext = {
546
- actor: actorContext,
547
- env,
548
- session: sessionDataForValues,
549
- };
550
- const staticValues = resolveFactoryValue(normalized.values, valueContext) || {};
551
- const builtHeaders = {
552
- ...(normalized.contentTypeJson ? { "Content-Type": "application/json" } : {}),
553
- ...staticValues,
554
- };
555
-
556
- const forwardedFor = buildForwardedForHeader(normalized.forwardedFor, env, actorContext);
557
- if (forwardedFor) {
558
- builtHeaders[forwardedFor.header] = forwardedFor.value;
559
- }
560
-
561
- for (const entry of normalized.fromSession || []) {
562
- const sessionData = resolveSessionData(setupData, entry);
563
- const value = sessionData ? readValueAtPath(sessionData, entry.field) : null;
564
- if (value != null && value !== "") {
565
- builtHeaders[entry.header] = String(value);
566
- }
567
- }
568
-
569
- return builtHeaders;
570
- };
571
- }
572
-
573
- function buildForwardedForHeader(config, env, actorName) {
574
- if (!config) return null;
575
- const header = typeof config === "object" && config.header ? config.header : "X-Forwarded-For";
576
- const seed =
577
- typeof config === "object" && config.seed
578
- ? config.seed
579
- : `${env?.BASE || "testkit"}:${actorName || "raw"}`;
580
- return {
581
- header,
582
- value: deriveDeterministicIp(seed),
583
- };
584
- }
585
-
586
- function deriveDeterministicIp(seed) {
587
- const text = String(seed || "testkit");
588
- const octets = [0, 0, 0, 0];
589
- for (let index = 0; index < text.length; index += 1) {
590
- octets[index % 4] = (octets[index % 4] * 31 + text.charCodeAt(index)) % 250;
591
- }
592
- return `10.${octets[0] + 1}.${octets[1] + 1}.${octets[2] + 1}`;
593
- }
594
-
595
- function extractCookieValue(response, cookieName) {
596
- const cookies = response?.cookies?.[cookieName];
597
- if (Array.isArray(cookies) && cookies[0]?.value) {
598
- return cookies[0].value;
599
- }
600
-
601
- const headerValues = [];
602
- for (const [headerName, headerValue] of Object.entries(response?.headers || {})) {
603
- if (String(headerName).toLowerCase() !== "set-cookie") continue;
604
- if (Array.isArray(headerValue)) headerValues.push(...headerValue);
605
- else if (headerValue) headerValues.push(headerValue);
606
- }
607
-
608
- const pattern = new RegExp(`(?:^|,\\s*)${cookieName}=([^;,]+)`);
609
- for (const value of headerValues) {
610
- const match = pattern.exec(String(value));
611
- if (match?.[1]) return match[1];
612
- }
613
-
614
- return null;
615
- }
616
-
617
- function safeRuntimeJson(response) {
618
- try {
619
- return runtimeJson(response);
620
- } catch {
621
- return null;
622
- }
623
- }
624
-
625
- function readValueAtPath(value, path) {
626
- if (value == null || typeof path !== "string" || path.trim().length === 0) {
627
- return null;
628
- }
629
- const parts = String(path)
630
- .replace(/\[(\d+)\]/g, ".$1")
631
- .split(".")
632
- .filter(Boolean);
633
-
634
- let current = value;
635
- for (const part of parts) {
636
- if (current == null) return null;
637
- current = current[part];
638
- }
639
- return current ?? null;
640
- }