@elench/testkit 0.1.25 → 0.1.27

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/lib/index.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type {
2
+ HttpClient,
3
+ HttpClientConfig,
4
+ RuntimeDb,
5
+ RuntimeDalContext,
6
+ RuntimeEnv,
7
+ RuntimeHeaders,
8
+ RuntimeOptions,
9
+ RuntimeResponse,
10
+ } from "./runtime/index";
11
+
12
+ export interface TestkitSuite<TSetup = unknown> {
13
+ options: RuntimeOptions;
14
+ setup: () => TSetup | null;
15
+ exec: (setupData: TSetup | null) => unknown;
16
+ }
17
+
18
+ export interface HeaderBuilderContext {
19
+ env: RuntimeEnv;
20
+ }
21
+
22
+ export type HeaderBuilder<TSetup = unknown> = (
23
+ setupData?: TSetup | null,
24
+ context?: HeaderBuilderContext
25
+ ) => RuntimeHeaders | void;
26
+
27
+ export interface AuthAdapter<TSetup = unknown> {
28
+ setup?: (context: { env: RuntimeEnv }) => TSetup;
29
+ headers?: HeaderBuilder<TSetup>;
30
+ }
31
+
32
+ export interface HttpSuiteContext<TSetup = unknown> {
33
+ env: RuntimeEnv;
34
+ req: HttpClient<TSetup>["request"];
35
+ rawReq: HttpClient["raw"];
36
+ getWithHeaders: HttpClient<TSetup>["getWithHeaders"];
37
+ setupData: TSetup | null;
38
+ session: TSetup | null;
39
+ }
40
+
41
+ export interface HttpSuiteConfig<TSetup = unknown> {
42
+ auth?: AuthAdapter<TSetup> | null;
43
+ env?: RuntimeEnv;
44
+ headers?: HeaderBuilder<TSetup>;
45
+ profile?: string;
46
+ rawHeaders?: HeaderBuilder<never>;
47
+ options?: RuntimeOptions;
48
+ }
49
+
50
+ export interface DalSuiteContext<TSetup = unknown> {
51
+ db: RuntimeDb;
52
+ dal: RuntimeDalContext;
53
+ setupData: TSetup | null;
54
+ }
55
+
56
+ export interface DalSuiteConfig<TSetup = unknown> {
57
+ db?: RuntimeDb;
58
+ options?: RuntimeOptions;
59
+ setup?: (context: { db: RuntimeDb; dal: RuntimeDalContext }) => TSetup;
60
+ }
61
+
62
+ export declare function defineHttpSuite<TSetup = unknown>(
63
+ run: (context: HttpSuiteContext<TSetup>) => unknown
64
+ ): TestkitSuite<TSetup>;
65
+
66
+ export declare function defineHttpSuite<TSetup = unknown>(
67
+ config: HttpSuiteConfig<TSetup>,
68
+ run: (context: HttpSuiteContext<TSetup>) => unknown
69
+ ): TestkitSuite<TSetup>;
70
+
71
+ export declare function defineDalSuite<TSetup = unknown>(
72
+ run: (context: DalSuiteContext<TSetup>) => unknown
73
+ ): TestkitSuite<TSetup>;
74
+
75
+ export declare function defineDalSuite<TSetup = unknown>(
76
+ config: DalSuiteConfig<TSetup>,
77
+ run: (context: DalSuiteContext<TSetup>) => unknown
78
+ ): TestkitSuite<TSetup>;
79
+
80
+ export declare function createAuthAdapter<TSetup = unknown>(
81
+ adapter?: AuthAdapter<TSetup>
82
+ ): AuthAdapter<TSetup>;
83
+
84
+ export type {
85
+ HttpClient,
86
+ HttpClientConfig,
87
+ RuntimeDb,
88
+ RuntimeDalContext,
89
+ RuntimeEnv,
90
+ RuntimeHeaders,
91
+ RuntimeOptions,
92
+ RuntimeResponse,
93
+ };
@@ -0,0 +1,29 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const packageJsonPath = path.join(rootDir, "package.json");
8
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
9
+
10
+ describe("package metadata", () => {
11
+ it("ships first-party type declarations for the public exports", () => {
12
+ expect(packageJson.types).toBe("./lib/index.d.ts");
13
+ expect(packageJson.exports["."]).toEqual({
14
+ types: "./lib/index.d.ts",
15
+ default: "./lib/index.mjs",
16
+ });
17
+ expect(packageJson.exports["./setup"]).toEqual({
18
+ types: "./lib/setup/index.d.ts",
19
+ default: "./lib/setup/index.mjs",
20
+ });
21
+ expect(packageJson.exports["./runtime"]).toEqual({
22
+ types: "./lib/runtime/index.d.ts",
23
+ default: "./lib/runtime/index.mjs",
24
+ });
25
+ expect(fs.existsSync(path.join(rootDir, "lib", "index.d.ts"))).toBe(true);
26
+ expect(fs.existsSync(path.join(rootDir, "lib", "setup", "index.d.ts"))).toBe(true);
27
+ expect(fs.existsSync(path.join(rootDir, "lib", "runtime", "index.d.ts"))).toBe(true);
28
+ });
29
+ });
@@ -832,12 +832,15 @@ function printRunSummary(results, durationMs) {
832
832
  );
833
833
  const failedSuites = executedServices.reduce((sum, result) => sum + result.failedSuiteCount, 0);
834
834
  const passedSuites = completedSuites - failedSuites;
835
+ const totalFiles = executedServices.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
836
+ const passedFiles = executedServices.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
835
837
 
836
838
  console.log("\n══ Summary ══");
837
839
  console.log(
838
840
  [
839
841
  `services ${passedServices.length}/${executedServices.length} passed`,
840
842
  `suites ${passedSuites}/${totalSuites} passed`,
843
+ totalFiles > 0 ? `files ${passedFiles}/${totalFiles} passed` : null,
841
844
  skippedServices.length > 0 ? `${skippedServices.length} skipped` : null,
842
845
  `duration ${formatDuration(durationMs)}`,
843
846
  ]
@@ -156,6 +156,19 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
156
156
  (suite) => suite.completedFileCount === suite.fileCount
157
157
  ).length;
158
158
  const failedSuiteCount = suites.filter((suite) => suite.failedFiles.length > 0).length;
159
+ const totalFileCount = suites.reduce((sum, suite) => sum + suite.fileCount, 0);
160
+ const completedFileCount = suites.reduce(
161
+ (sum, suite) => sum + suite.completedFileCount,
162
+ 0
163
+ );
164
+ const failedFileCount = suites.reduce((sum, suite) => sum + suite.failedFiles.length, 0);
165
+ const passedFileCount = suites.reduce(
166
+ (sum, suite) =>
167
+ sum +
168
+ suite.fileResults.filter((file) => file.status === "passed").length,
169
+ 0
170
+ );
171
+ const notRunFileCount = totalFileCount - completedFileCount;
159
172
  const accumulatedDurationMs = suites.reduce((sum, suite) => sum + suite.durationMs, 0);
160
173
  const durationMs =
161
174
  tracker.firstTaskAt && tracker.lastTaskAt
@@ -173,6 +186,11 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
173
186
  suiteCount: tracker.suiteCount,
174
187
  completedSuiteCount,
175
188
  failedSuiteCount,
189
+ totalFileCount,
190
+ completedFileCount,
191
+ passedFileCount,
192
+ failedFileCount,
193
+ notRunFileCount,
176
194
  durationMs,
177
195
  suites: suites.map((suite) => ({
178
196
  name: suite.name,
@@ -180,6 +198,10 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
180
198
  framework: formatFrameworkForArtifact(suite.framework),
181
199
  failed: suite.failedFiles.length > 0,
182
200
  fileCount: suite.fileCount,
201
+ completedFileCount: suite.completedFileCount,
202
+ passedFileCount: suite.fileResults.filter((file) => file.status === "passed").length,
203
+ failedFileCount: suite.failedFiles.length,
204
+ notRunFileCount: suite.fileCount - suite.completedFileCount,
183
205
  failedFiles: suite.failedFiles,
184
206
  durationMs: suite.durationMs,
185
207
  error: suite.error,
@@ -299,6 +321,10 @@ export function buildRunArtifact({
299
321
  const totalSuites = executed.reduce((sum, result) => sum + result.suiteCount, 0);
300
322
  const completedSuites = executed.reduce((sum, result) => sum + result.completedSuiteCount, 0);
301
323
  const failedSuites = executed.reduce((sum, result) => sum + result.failedSuiteCount, 0);
324
+ const totalFiles = executed.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
325
+ const passedFiles = executed.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
326
+ const failedFiles = executed.reduce((sum, result) => sum + (result.failedFileCount || 0), 0);
327
+ const notRunFiles = executed.reduce((sum, result) => sum + (result.notRunFileCount || 0), 0);
302
328
  const dbBackend = summarizeDbBackend(results);
303
329
 
304
330
  return {
@@ -339,6 +365,12 @@ export function buildRunArtifact({
339
365
  passed: completedSuites - failedSuites,
340
366
  failed: failedSuites,
341
367
  },
368
+ files: {
369
+ total: totalFiles,
370
+ passed: passedFiles,
371
+ failed: failedFiles,
372
+ notRun: notRunFiles,
373
+ },
342
374
  },
343
375
  services: results.map((result) => ({
344
376
  name: result.name,
@@ -347,6 +379,11 @@ export function buildRunArtifact({
347
379
  suiteCount: result.suiteCount,
348
380
  completedSuiteCount: result.completedSuiteCount,
349
381
  failedSuiteCount: result.failedSuiteCount,
382
+ totalFileCount: result.totalFileCount,
383
+ completedFileCount: result.completedFileCount,
384
+ passedFileCount: result.passedFileCount,
385
+ failedFileCount: result.failedFileCount,
386
+ notRunFileCount: result.notRunFileCount,
350
387
  durationMs: result.durationMs,
351
388
  dbBackend: result.dbBackend,
352
389
  suites: result.suites,
@@ -372,10 +409,15 @@ export function formatDuration(durationMs) {
372
409
 
373
410
  export function formatServiceSummary(result) {
374
411
  const passedSuites = result.completedSuiteCount - result.failedSuiteCount;
375
- const notRun = result.suiteCount - result.completedSuiteCount;
412
+ const notRunSuites = result.suiteCount - result.completedSuiteCount;
376
413
  let detail = `${passedSuites}/${result.suiteCount} suites passed`;
377
- if (notRun > 0) {
378
- detail += `, ${notRun} not run`;
414
+ if ((result.totalFileCount || 0) > 0) {
415
+ detail += `, ${result.passedFileCount}/${result.totalFileCount} files passed`;
416
+ }
417
+ if (notRunSuites > 0) {
418
+ detail += `, ${notRunSuites} ${pluralize(notRunSuites, "suite", "suites")} not run`;
419
+ } else if ((result.notRunFileCount || 0) > 0) {
420
+ detail += `, ${result.notRunFileCount} ${pluralize(result.notRunFileCount, "file", "files")} not run`;
379
421
  }
380
422
  return detail;
381
423
  }
@@ -404,3 +446,7 @@ function sanitizeErrorMessage(message) {
404
446
  .replace(/Command failed with exit code (\d+): k6 run\b/g, "Default runtime failed with exit code $1:")
405
447
  .replace(/[\\/]vendor[\\/]k6\b/g, "default-runtime");
406
448
  }
449
+
450
+ function pluralize(value, singular, plural) {
451
+ return value === 1 ? singular : plural;
452
+ }
@@ -64,8 +64,12 @@ describe("runner-results", () => {
64
64
  const result = finalizeServiceResult(tracker, 1000, 1500);
65
65
  expect(result.failed).toBe(true);
66
66
  expect(result.failedSuiteCount).toBe(1);
67
+ expect(result.totalFileCount).toBe(1);
68
+ expect(result.failedFileCount).toBe(1);
69
+ expect(result.passedFileCount).toBe(0);
67
70
  expect(result.errors).toEqual(["worker failed", "graph failed"]);
68
71
  expect(result.suites[0].framework).toBe("default");
72
+ expect(result.suites[0].failedFileCount).toBe(1);
69
73
  expect(result.suites[0].files).toEqual([
70
74
  {
71
75
  path: "tests/health.js",
@@ -86,6 +90,11 @@ describe("runner-results", () => {
86
90
  suiteCount: 1,
87
91
  completedSuiteCount: 1,
88
92
  failedSuiteCount: 0,
93
+ totalFileCount: 3,
94
+ completedFileCount: 3,
95
+ passedFileCount: 3,
96
+ failedFileCount: 0,
97
+ notRunFileCount: 0,
89
98
  durationMs: 1200,
90
99
  dbBackend: "local",
91
100
  suites: [],
@@ -98,6 +107,11 @@ describe("runner-results", () => {
98
107
  suiteCount: 0,
99
108
  completedSuiteCount: 0,
100
109
  failedSuiteCount: 0,
110
+ totalFileCount: 0,
111
+ completedFileCount: 0,
112
+ passedFileCount: 0,
113
+ failedFileCount: 0,
114
+ notRunFileCount: 0,
101
115
  durationMs: 0,
102
116
  dbBackend: null,
103
117
  suites: [],
@@ -134,6 +148,12 @@ describe("runner-results", () => {
134
148
 
135
149
  expect(artifact.product.name).toBe("my-product");
136
150
  expect(artifact.summary.services.total).toBe(1);
151
+ expect(artifact.summary.files).toEqual({
152
+ total: 3,
153
+ passed: 3,
154
+ failed: 0,
155
+ notRun: 0,
156
+ });
137
157
  expect(summarizeDbBackend(results)).toBe("local");
138
158
  expect(formatDuration(65_000)).toBe("1m 05s");
139
159
  expect(
@@ -141,8 +161,11 @@ describe("runner-results", () => {
141
161
  completedSuiteCount: 2,
142
162
  failedSuiteCount: 1,
143
163
  suiteCount: 3,
164
+ totalFileCount: 6,
165
+ passedFileCount: 5,
166
+ notRunFileCount: 1,
144
167
  })
145
- ).toBe("1/3 suites passed, 1 not run");
168
+ ).toBe("1/3 suites passed, 5/6 files passed, 1 suite not run");
146
169
  expect(formatError(new Error("boom"))).toBe("boom");
147
170
  });
148
171
 
@@ -72,7 +72,7 @@ export function buildPortMap(runtimeConfigs, workerId) {
72
72
  if (existing) {
73
73
  throw new Error(
74
74
  `Worker port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
75
- `Assign distinct local.port/baseUrl ports in testkit.config.json.`
75
+ `Assign distinct local.port/baseUrl ports in testkit.setup.ts.`
76
76
  );
77
77
  }
78
78
  seen.set(actualPort, config.name);
@@ -0,0 +1,183 @@
1
+ export type RuntimeMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+
3
+ export interface RuntimeHeaders {
4
+ [key: string]: string;
5
+ }
6
+
7
+ export interface RuntimeCookie {
8
+ value: string;
9
+ }
10
+
11
+ export interface RuntimeResponse {
12
+ body: string;
13
+ cookies?: Record<string, RuntimeCookie[]>;
14
+ headers?: Record<string, string | string[] | undefined>;
15
+ status: number;
16
+ timings?: {
17
+ duration: number;
18
+ };
19
+ }
20
+
21
+ export interface RuntimeOptions {
22
+ [key: string]: unknown;
23
+ thresholds?: Record<string, unknown>;
24
+ }
25
+
26
+ export interface RuntimeEnv {
27
+ BASE: string;
28
+ MACHINE_ID?: string;
29
+ routeParams: RuntimeHeaders;
30
+ }
31
+
32
+ export interface RuntimeDb {
33
+ exec(sql: string): unknown;
34
+ query<T = Record<string, unknown>>(sql: string): T[];
35
+ }
36
+
37
+ export interface RuntimeDalContext {
38
+ db: RuntimeDb;
39
+ truncate(...tables: string[]): void;
40
+ }
41
+
42
+ export interface HttpRequestParams {
43
+ headers?: RuntimeHeaders;
44
+ redirects?: number;
45
+ }
46
+
47
+ export interface RuntimeHttpClient {
48
+ del(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
49
+ file(data: unknown, filename?: string, contentType?: string): unknown;
50
+ get(url: string, params?: HttpRequestParams): RuntimeResponse;
51
+ patch(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
52
+ post(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
53
+ put(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
54
+ }
55
+
56
+ export interface Metric {
57
+ add(value: number): void;
58
+ }
59
+
60
+ export declare class Rate implements Metric {
61
+ constructor(name: string, isTime?: boolean);
62
+ add(value: number): void;
63
+ }
64
+
65
+ export declare class Trend implements Metric {
66
+ constructor(name: string, isTime?: boolean);
67
+ add(value: number): void;
68
+ }
69
+
70
+ export interface HttpClientConfig<TSetup = unknown> {
71
+ baseUrl: string;
72
+ defaultHeaders?: RuntimeHeaders;
73
+ getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void;
74
+ getRawHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void;
75
+ routeHeaders?: RuntimeHeaders;
76
+ }
77
+
78
+ export interface HttpClient<TSetup = unknown> {
79
+ delete(path: string, setupData?: TSetup | null, extraHeaders?: RuntimeHeaders): RuntimeResponse;
80
+ get(path: string, setupData?: TSetup | null, extraHeaders?: RuntimeHeaders): RuntimeResponse;
81
+ getWithHeaders(
82
+ path: string,
83
+ setupData?: TSetup | null,
84
+ extraHeaders?: RuntimeHeaders
85
+ ): RuntimeResponse;
86
+ patch(
87
+ path: string,
88
+ setupData?: TSetup | null,
89
+ body?: unknown,
90
+ extraHeaders?: RuntimeHeaders
91
+ ): RuntimeResponse;
92
+ post(
93
+ path: string,
94
+ setupData?: TSetup | null,
95
+ body?: unknown,
96
+ extraHeaders?: RuntimeHeaders
97
+ ): RuntimeResponse;
98
+ put(
99
+ path: string,
100
+ setupData?: TSetup | null,
101
+ body?: unknown,
102
+ extraHeaders?: RuntimeHeaders
103
+ ): RuntimeResponse;
104
+ raw(
105
+ method: RuntimeMethod,
106
+ path: string,
107
+ body?: unknown,
108
+ extraHeaders?: RuntimeHeaders
109
+ ): RuntimeResponse;
110
+ rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
111
+ rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
112
+ rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
113
+ rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
114
+ rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
115
+ request(
116
+ method: RuntimeMethod,
117
+ path: string,
118
+ setupData?: TSetup | null,
119
+ body?: unknown,
120
+ extraHeaders?: RuntimeHeaders
121
+ ): RuntimeResponse;
122
+ }
123
+
124
+ export declare const check: <T>(
125
+ value: T,
126
+ checks: Record<string, (value: T) => boolean>
127
+ ) => boolean;
128
+ export declare const fail: (message: string) => never;
129
+ export declare const group: (name: string, fn: () => void) => void;
130
+ export declare const sleep: (seconds?: number) => void;
131
+
132
+ export declare const http: RuntimeHttpClient;
133
+
134
+ export declare function file(data: unknown, filename?: string, contentType?: string): unknown;
135
+ export declare function json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
136
+ export declare function contains<T extends Record<string, unknown>>(
137
+ rows: T[],
138
+ field: keyof T | string,
139
+ value: unknown
140
+ ): boolean;
141
+ export declare function allMatch<T>(
142
+ rows: T[],
143
+ predicate: (row: T) => boolean
144
+ ): boolean;
145
+ export declare function isSorted<T extends Record<string, unknown>>(
146
+ rows: T[],
147
+ field: keyof T | string,
148
+ direction?: "asc" | "desc"
149
+ ): boolean;
150
+
151
+ export declare function singleIterationOptions(overrides?: RuntimeOptions): RuntimeOptions;
152
+ export declare const defaultOptions: RuntimeOptions;
153
+ export declare const httpDefaultOptions: RuntimeOptions;
154
+
155
+ export declare function createDalContext(db?: RuntimeDb): RuntimeDalContext;
156
+ export declare function openDb(): RuntimeDb;
157
+ export declare function truncate(db: RuntimeDb, ...tables: string[]): void;
158
+
159
+ export declare function getEnv(): RuntimeEnv;
160
+ export declare function createHttpClient<TSetup = unknown>(
161
+ config: HttpClientConfig<TSetup>
162
+ ): HttpClient<TSetup>;
163
+ export declare function makeReq<TSetup = unknown>(
164
+ baseUrl: string,
165
+ routeHeaders?: RuntimeHeaders,
166
+ getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
167
+ ): HttpClient<TSetup>["request"];
168
+ export declare function makeRawReq(
169
+ baseUrl: string,
170
+ routeHeaders?: RuntimeHeaders,
171
+ getRawHeaders?: (setupData?: never) => RuntimeHeaders | void
172
+ ): HttpClient["raw"];
173
+ export declare function makeGetWithHeaders<TSetup = unknown>(
174
+ baseUrl: string,
175
+ routeHeaders?: RuntimeHeaders,
176
+ getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
177
+ ): HttpClient<TSetup>["getWithHeaders"];
178
+
179
+ declare global {
180
+ const __ENV: Record<string, string | undefined>;
181
+ }
182
+
183
+ export {};
@@ -56,6 +56,7 @@ export function createHttpClient(config) {
56
56
  }
57
57
 
58
58
  return {
59
+ rawHttp: http,
59
60
  request,
60
61
  raw,
61
62
  get(path, setupData, extraHeaders = {}) {
@@ -1,50 +1,49 @@
1
1
  import { fail } from "k6";
2
2
  import { defaultOptions, recordRuntimeFailure } from "./checks.js";
3
3
  import { createHttpClient, getEnv } from "./http.js";
4
+ import {
5
+ clearRuntimeContext,
6
+ registerRuntimeContext,
7
+ resolveHttpProfile,
8
+ } from "../../setup/runtime.mjs";
4
9
 
5
10
  export function defineHttpSuite(configOrRun, maybeRun) {
6
11
  const { config, run } = normalizeSuiteArgs(configOrRun, maybeRun);
7
- const env = config.env || getEnv();
8
- const auth = config.auth || null;
9
-
10
- const client = createHttpClient({
11
- baseUrl: env.BASE,
12
- routeHeaders: env.routeParams,
13
- getHeaders(setupData) {
14
- return {
15
- ...callHeaders(auth?.headers, setupData, env),
16
- ...callHeaders(config.headers, setupData, env),
17
- };
18
- },
19
- getRawHeaders(setupData) {
20
- return callHeaders(config.rawHeaders, setupData, env);
21
- },
22
- });
23
12
 
24
13
  return {
25
- options: config.options || defaultOptions,
14
+ get options() {
15
+ return mergeProfileConfig(config).options || defaultOptions;
16
+ },
26
17
  setup() {
27
- if (typeof auth?.setup !== "function") return null;
18
+ const resolved = resolveRuntimeConfig(config);
28
19
  try {
29
- return auth.setup({ env });
20
+ registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
21
+ if (typeof resolved.auth?.setup !== "function") return null;
22
+ return resolved.auth.setup({ env: resolved.env });
30
23
  } catch (error) {
31
24
  recordRuntimeFailure();
32
25
  fail(formatFatalSuiteError("setup", error));
26
+ } finally {
27
+ clearRuntimeContext();
33
28
  }
34
29
  },
35
30
  exec(setupData) {
31
+ const resolved = resolveRuntimeConfig(config);
36
32
  try {
33
+ registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
37
34
  return run({
38
- env,
39
- req: client.request,
40
- rawReq: client.raw,
41
- getWithHeaders: client.getWithHeaders,
35
+ env: resolved.env,
36
+ req: resolved.client.request,
37
+ rawReq: resolved.client.raw,
38
+ getWithHeaders: resolved.client.getWithHeaders,
42
39
  setupData,
43
40
  session: setupData,
44
41
  });
45
42
  } catch (error) {
46
43
  recordRuntimeFailure();
47
44
  fail(formatFatalSuiteError("exec", error));
45
+ } finally {
46
+ clearRuntimeContext();
48
47
  }
49
48
  },
50
49
  };
@@ -65,6 +64,50 @@ function callHeaders(builder, setupData, env) {
65
64
  return builder(setupData, { env }) || {};
66
65
  }
67
66
 
67
+ function mergeProfileConfig(config) {
68
+ if (!config?.profile) return config || {};
69
+
70
+ const profile = resolveHttpProfile(config.profile) || {};
71
+ return {
72
+ ...profile,
73
+ ...config,
74
+ auth: config.auth ?? profile.auth ?? null,
75
+ headers: config.headers ?? profile.headers,
76
+ rawHeaders: config.rawHeaders ?? profile.rawHeaders,
77
+ options: config.options ?? profile.options,
78
+ env: config.env ?? profile.env,
79
+ };
80
+ }
81
+
82
+ function resolveRuntimeConfig(config) {
83
+ const resolvedConfig = mergeProfileConfig(config);
84
+ const env = {
85
+ ...(resolvedConfig.env || getEnv()),
86
+ rawEnv: __ENV,
87
+ };
88
+ const auth = resolvedConfig.auth || null;
89
+ const client = createHttpClient({
90
+ baseUrl: env.BASE,
91
+ routeHeaders: env.routeParams,
92
+ getHeaders(setupData) {
93
+ return {
94
+ ...callHeaders(auth?.headers, setupData, env),
95
+ ...callHeaders(resolvedConfig.headers, setupData, env),
96
+ };
97
+ },
98
+ getRawHeaders(setupData) {
99
+ return callHeaders(resolvedConfig.rawHeaders, setupData, env);
100
+ },
101
+ });
102
+
103
+ return {
104
+ resolvedConfig,
105
+ env,
106
+ auth,
107
+ client,
108
+ };
109
+ }
110
+
68
111
  function formatFatalSuiteError(phase, error) {
69
112
  if (error instanceof Error) {
70
113
  return `Uncaught testkit suite error during ${phase}: ${error.message}`;