@elench/testkit 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -365,8 +365,31 @@ Named HTTP profiles live in `testkit.config.ts` and can be referenced by name:
365
365
 
366
366
  ```ts
367
367
  import { defineHttpSuite } from "@elench/testkit";
368
+ import { defineConfig, profiles } from "@elench/testkit/config";
368
369
 
369
- const suite = defineHttpSuite({ profile: "default-auth" }, ({ req, setupData }) => {
370
+ export default defineConfig({
371
+ profiles: {
372
+ http: {
373
+ defaultAuth: profiles.localJson({
374
+ password: "password",
375
+ identities: {
376
+ primary: {
377
+ email: "test@example.com",
378
+ },
379
+ },
380
+ session: {
381
+ authCookie: "session",
382
+ },
383
+ headers: {
384
+ contentTypeJson: true,
385
+ forwardedFor: "deterministic",
386
+ },
387
+ }).session(),
388
+ },
389
+ },
390
+ });
391
+
392
+ const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ req, setupData }) => {
370
393
  req("GET", "/api/auth/session", setupData);
371
394
  });
372
395
  ```
@@ -4,6 +4,7 @@ import ts from "typescript";
4
4
  import { discoverTests } from "../discovery/index.mjs";
5
5
  import { loadConfigContext } from "../config/index.mjs";
6
6
  import { runTestkitTypecheck } from "./typecheck.mjs";
7
+ import { findConfigFile } from "../config/config-loader.mjs";
7
8
 
8
9
  export async function runDoctor(options = {}) {
9
10
  const checks = [];
@@ -39,6 +40,17 @@ export async function runDoctor(options = {}) {
39
40
  details: playwrightViolations,
40
41
  });
41
42
 
43
+ const configImportViolations = findConfigImportViolations(productDir);
44
+ checks.push({
45
+ code: "config-import-hygiene",
46
+ level: configImportViolations.length === 0 ? "pass" : "fail",
47
+ message:
48
+ configImportViolations.length === 0
49
+ ? "Repo config does not import __testkit__ helper modules"
50
+ : `Found ${configImportViolations.length} repo config import violation(s)`,
51
+ details: configImportViolations,
52
+ });
53
+
42
54
  const hasBrowserOrNextWork = discovery.files.some((entry) => entry.selectionType === "pw");
43
55
  if (hasBrowserOrNextWork) {
44
56
  const nodeCount = discovery.coverageGraph?.nodes?.length || 0;
@@ -113,6 +125,31 @@ function findPlaywrightRuntimeImportViolations(productDir) {
113
125
  return violations;
114
126
  }
115
127
 
128
+ function findConfigImportViolations(productDir) {
129
+ const configFile = findConfigFile(productDir);
130
+ if (!configFile || !fs.existsSync(configFile)) return [];
131
+
132
+ const sourceText = fs.readFileSync(configFile, "utf8");
133
+ const sourceFile = ts.createSourceFile(configFile, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
134
+ const violations = [];
135
+
136
+ for (const statement of sourceFile.statements) {
137
+ if (!ts.isImportDeclaration(statement)) continue;
138
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
139
+ const specifier = statement.moduleSpecifier.text;
140
+ if (!isRepoLocalConfigImportViolation(specifier)) continue;
141
+ const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
142
+ violations.push({
143
+ file: path.relative(productDir, configFile).split(path.sep).join("/"),
144
+ line: position.line + 1,
145
+ specifier,
146
+ snippet: statement.getText(sourceFile),
147
+ });
148
+ }
149
+
150
+ return violations;
151
+ }
152
+
116
153
  function collectFiles(rootDir, out = []) {
117
154
  if (!fs.existsSync(rootDir)) return out;
118
155
  for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
@@ -129,6 +166,12 @@ function collectFiles(rootDir, out = []) {
129
166
  return out.sort((left, right) => left.localeCompare(right));
130
167
  }
131
168
 
169
+ function isRepoLocalConfigImportViolation(specifier) {
170
+ if (typeof specifier !== "string") return false;
171
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) return false;
172
+ return specifier.includes("__testkit__");
173
+ }
174
+
132
175
  function relativeViolation(productDir, absolutePath, sourceFile, statement) {
133
176
  const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
134
177
  return {
@@ -1,12 +1,21 @@
1
1
  import { createColors } from "picocolors";
2
+ import figures from "figures";
2
3
 
3
4
  const pc = createColors(Boolean(process.stdout?.isTTY || process.env.FORCE_COLOR));
4
5
 
5
6
  export function colorStatus(status) {
6
- if (status === "PASS") return pc.green(status);
7
- if (status === "FAIL") return pc.red(status);
8
- if (status === "SKIP") return pc.yellow(status);
9
- if (status === "RUN") return pc.cyan(status);
7
+ if (status === "PASS") return pc.green(figures.tick);
8
+ if (status === "FAIL") return pc.red(figures.cross);
9
+ if (status === "SKIP") return pc.yellow(figures.arrowDown);
10
+ if (status === "RUN") return pc.cyan(figures.play);
11
+ return status;
12
+ }
13
+
14
+ export function statusLabel(status) {
15
+ if (status === "PASS") return pc.green(`${figures.tick} PASS`);
16
+ if (status === "FAIL") return pc.red(`${figures.cross} FAIL`);
17
+ if (status === "SKIP") return pc.yellow(`${figures.arrowDown} SKIP`);
18
+ if (status === "RUN") return pc.cyan(`${figures.play} RUN`);
10
19
  return status;
11
20
  }
12
21
 
@@ -18,6 +27,14 @@ export function bold(text) {
18
27
  return pc.bold(text);
19
28
  }
20
29
 
30
+ export function boldRed(text) {
31
+ return pc.bold(pc.red(text));
32
+ }
33
+
34
+ export function red(text) {
35
+ return pc.red(text);
36
+ }
37
+
21
38
  export function muted(text) {
22
39
  return pc.dim(text);
23
40
  }
@@ -5,13 +5,18 @@ import {
5
5
  colorDiagnosticSeverity,
6
6
  colorHeading,
7
7
  colorService,
8
- colorStatus,
9
8
  colorTypeBadge,
10
9
  muted,
10
+ statusLabel,
11
11
  } from "./colors.mjs";
12
12
 
13
13
  const TYPE_ORDER = ["int", "e2e", "scenario", "dal", "load", "pw"];
14
14
 
15
+ const TREE_BRANCH = "\u251C\u2500\u2500 ";
16
+ const TREE_LAST = "\u2514\u2500\u2500 ";
17
+ const TREE_PIPE = "\u2502 ";
18
+ const TREE_SPACE = " ";
19
+
15
20
  export function buildDiscoveryReportLines(result, options = {}) {
16
21
  const mode = options.outputMode || "compact";
17
22
  return mode === "verbose" ? buildVerboseLines(result) : buildCompactLines(result);
@@ -20,7 +25,7 @@ export function buildDiscoveryReportLines(result, options = {}) {
20
25
  function buildCompactLines(result) {
21
26
  const lines = [];
22
27
  lines.push(
23
- `${colorHeading("Summary")} ${result.summary.files} files · ${result.summary.activeFiles} active · ${result.summary.skippedFiles} skipped · ${result.summary.suites} suites · ${result.summary.services} services`
28
+ `${colorHeading("Summary")} ${result.summary.files} files \u00B7 ${result.summary.activeFiles} active \u00B7 ${result.summary.skippedFiles} skipped \u00B7 ${result.summary.suites} suites \u00B7 ${result.summary.services} services`
24
29
  );
25
30
 
26
31
  if (result.history?.available) {
@@ -35,17 +40,33 @@ function buildCompactLines(result) {
35
40
  for (const service of result.services) {
36
41
  lines.push("");
37
42
  lines.push(
38
- `${colorService(service.name)} ${muted(`(${service.fileCount} files${service.dependsOn.length > 0 ? ` · depends on ${service.dependsOn.join(", ")}` : ""})`)}`
43
+ `${colorService(service.name)} ${muted(`(${service.fileCount} files${service.dependsOn.length > 0 ? ` \u00B7 depends on ${service.dependsOn.join(", ")}` : ""})`)}`
39
44
  );
40
45
  const typeGroups = suitesByService.get(service.name) || new Map();
41
- for (const type of orderedTypes([...typeGroups.keys()])) {
46
+ const types = orderedTypes([...typeGroups.keys()]);
47
+
48
+ for (let ti = 0; ti < types.length; ti++) {
49
+ const type = types[ti];
50
+ const isLastType = ti === types.length - 1;
51
+ const typeConnector = isLastType ? TREE_LAST : TREE_BRANCH;
52
+ const typePrefix = isLastType ? TREE_SPACE : TREE_PIPE;
42
53
  const suites = typeGroups.get(type) || [];
43
- lines.push(` ${colorTypeBadge(type.toUpperCase())} ${formatSelectionTypeLabel(type)}`);
44
- for (const suite of suites) {
45
- lines.push(` ${bold(suite.groupLabel)} ${muted(`(${suite.fileCount} files)`)}`);
46
- for (const file of filesBySuite.get(suite.id) || []) {
47
- const status = file.skipped ? `${colorStatus("SKIP")} ${file.skipReason}` : muted(buildHistoryHint(file));
48
- lines.push(` ${file.displayName}${status ? ` ${status}` : ""}`);
54
+ lines.push(`${typeConnector}${colorTypeBadge(type.toUpperCase())} ${formatSelectionTypeLabel(type)}`);
55
+
56
+ for (let si = 0; si < suites.length; si++) {
57
+ const suite = suites[si];
58
+ const isLastSuite = si === suites.length - 1;
59
+ const suiteConnector = isLastSuite ? TREE_LAST : TREE_BRANCH;
60
+ const suitePrefix = isLastSuite ? TREE_SPACE : TREE_PIPE;
61
+ lines.push(`${typePrefix}${suiteConnector}${bold(suite.groupLabel)} ${muted(`(${suite.fileCount} files)`)}`);
62
+
63
+ const files = filesBySuite.get(suite.id) || [];
64
+ for (let fi = 0; fi < files.length; fi++) {
65
+ const file = files[fi];
66
+ const isLastFile = fi === files.length - 1;
67
+ const fileConnector = isLastFile ? TREE_LAST : TREE_BRANCH;
68
+ const status = file.skipped ? `${statusLabel("SKIP")} ${file.skipReason}` : muted(buildHistoryHint(file));
69
+ lines.push(`${typePrefix}${suitePrefix}${fileConnector}${file.displayName}${status ? ` ${status}` : ""}`);
49
70
  }
50
71
  }
51
72
  }
@@ -73,7 +94,7 @@ function buildVerboseLines(result) {
73
94
  if (service.dependsOn.length > 0) {
74
95
  lines.push(` dependsOn ${service.dependsOn.join(", ")}`);
75
96
  }
76
- lines.push(` files ${service.fileCount} · active ${service.activeFileCount} · skipped ${service.skippedFileCount}`);
97
+ lines.push(` files ${service.fileCount} \u00B7 active ${service.activeFileCount} \u00B7 skipped ${service.skippedFileCount}`);
77
98
 
78
99
  const suites = result.suites.filter((suite) => suite.service === service.name).sort(compareSuites);
79
100
  for (const suite of suites) {
@@ -162,5 +183,5 @@ function buildHistoryHint(file) {
162
183
  file.history.firstSeenAt ? `seen ${file.history.firstSeenAt.slice(0, 10)}` : null,
163
184
  ]
164
185
  .filter(Boolean)
165
- .join(" · ");
186
+ .join(" \u00B7 ");
166
187
  }
@@ -1,15 +1,89 @@
1
1
  import path from "path";
2
+ import figures from "figures";
2
3
  import {
3
4
  buildCompactRunSummaryLines,
4
5
  buildDebugRunSummaryLines,
5
6
  formatDuration,
6
7
  } from "../../runner/formatting.mjs";
7
- import { colorSectionLine, colorStatus, dim } from "./colors.mjs";
8
+ import { boldRed, colorSectionLine, dim, red, statusLabel } from "./colors.mjs";
9
+
10
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
11
+ const DURATION_RE = /\b(\d+m\s+\d+s|\d+s)\b/g;
12
+
13
+ function stripAnsi(text) {
14
+ return text.replace(ANSI_RE, "");
15
+ }
16
+
17
+ function boxLines(lines) {
18
+ const nonEmpty = lines.filter((l) => stripAnsi(l).trim().length > 0);
19
+ if (nonEmpty.length === 0) return lines;
20
+ const maxWidth = lines.reduce((max, l) => Math.max(max, stripAnsi(l).length), 0);
21
+ const pad = 1;
22
+ const innerWidth = maxWidth + pad * 2;
23
+ const top = `${figures.lineDownRight}${figures.line.repeat(innerWidth)}${figures.lineDownLeft}`;
24
+ const bottom = `${figures.lineUpRight}${figures.line.repeat(innerWidth)}${figures.lineUpLeft}`;
25
+ const boxed = [top];
26
+ for (const line of lines) {
27
+ const visible = stripAnsi(line).length;
28
+ const right = innerWidth - pad - visible;
29
+ boxed.push(`${figures.lineVertical}${" ".repeat(pad)}${line}${" ".repeat(Math.max(0, right))}${figures.lineVertical}`);
30
+ }
31
+ boxed.push(bottom);
32
+ return boxed;
33
+ }
34
+
35
+ function colorFailureSummaryLines(lines) {
36
+ const result = [];
37
+ let inFailuresSection = false;
38
+ let lastWasFilePath = false;
39
+ for (const line of lines) {
40
+ const raw = stripAnsi(line);
41
+ if (raw === "Failures:" || raw === "Runtime Errors:") {
42
+ inFailuresSection = true;
43
+ lastWasFilePath = false;
44
+ result.push(line);
45
+ continue;
46
+ }
47
+ if (/^(Summary:|Result:|Known-failure|Triage:)/.test(raw) || raw === "") {
48
+ inFailuresSection = false;
49
+ lastWasFilePath = false;
50
+ }
51
+ if (inFailuresSection) {
52
+ if (/^ {2}\S/.test(raw)) {
53
+ lastWasFilePath = true;
54
+ result.push(boldRed(raw));
55
+ continue;
56
+ }
57
+ if (/^ {4}\S/.test(raw)) {
58
+ if (lastWasFilePath) {
59
+ lastWasFilePath = false;
60
+ result.push(boldRed(raw));
61
+ } else {
62
+ result.push(red(raw));
63
+ }
64
+ continue;
65
+ }
66
+ }
67
+ lastWasFilePath = false;
68
+ result.push(line);
69
+ }
70
+ return result;
71
+ }
72
+
73
+ function dimDurations(line) {
74
+ return line.replace(DURATION_RE, (match) => dim(match));
75
+ }
8
76
 
9
77
  export function createRunReporter({ outputMode = "compact", stdout = process.stdout, stderr = process.stderr } = {}) {
10
78
  const mode = outputMode || "compact";
79
+ let completedCount = 0;
80
+ let totalFileCount = 0;
81
+
11
82
  return {
12
83
  outputMode: mode,
84
+ setTotalFileCount(count) {
85
+ totalFileCount = count;
86
+ },
13
87
  writeLine(line = "") {
14
88
  stdout.write(`${line}\n`);
15
89
  },
@@ -34,7 +108,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
34
108
  if (operation.status === "failed") {
35
109
  const detail = shortenMessage(operation.error || operation.summary || operation.stage);
36
110
  stdout.write(
37
- `${colorStatus("FAIL")} ${"SETUP"} ${operation.serviceName} ${operation.stage} ${dim(duration)} ${detail}\n`
111
+ `${statusLabel("FAIL")} ${"SETUP"} ${operation.serviceName} ${operation.stage} ${dim(duration)} ${boldRed(detail)}\n`
38
112
  );
39
113
  return;
40
114
  }
@@ -44,7 +118,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
44
118
  (operation.durationMs || 0) >= 5_000
45
119
  ) {
46
120
  const summary = shortenMessage(operation.summary || operation.stage);
47
- stdout.write(`${colorStatus("RUN")} ${"SETUP"} ${operation.serviceName} ${summary} ${dim(duration)}\n`);
121
+ stdout.write(`${statusLabel("RUN")} ${"SETUP"} ${operation.serviceName} ${summary} ${dim(duration)}\n`);
48
122
  }
49
123
  },
50
124
  localServiceStarting(config, command) {
@@ -52,19 +126,20 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
52
126
  stdout.write(`Starting ${config.runtimeLabel}:${config.name}: ${command}\n`);
53
127
  },
54
128
  serviceSkipped(config, reason) {
55
- stdout.write(`${colorStatus("SKIP")} ${config.name} ${reason}\n`);
129
+ stdout.write(`${statusLabel("SKIP")} ${config.name} ${reason}\n`);
56
130
  },
57
131
  plannedSkip(entry) {
58
132
  stdout.write(
59
- `${colorStatus("SKIP")} ${entry.serviceName} ${entry.type} ${normalizePath(entry.file)} ${dim("0s")} ${shortenMessage(entry.reason || "skipped")}\n`
133
+ `${statusLabel("SKIP")} ${entry.serviceName} ${entry.type} ${normalizePath(entry.file)} ${dim("0s")} ${shortenMessage(entry.reason || "skipped")}\n`
60
134
  );
61
135
  },
62
136
  taskStarted(task, targetConfig) {
63
137
  if (mode !== "debug") return;
64
- stdout.write(`${colorStatus("RUN").padEnd(12)} ${targetConfig.name} ${task.type} ${task.file}\n`);
138
+ stdout.write(`${statusLabel("RUN")} ${targetConfig.name} ${task.type} ${task.file}\n`);
65
139
  },
66
140
  taskFinished(task, outcome) {
67
141
  if (mode === "json") return;
142
+ completedCount += 1;
68
143
  const status = outcome.status === "skipped" ? "SKIP" : outcome.failed ? "FAIL" : "PASS";
69
144
  const duration = formatDuration(outcome.durationMs || 0);
70
145
  const primaryFailure = firstFailureDetail(outcome);
@@ -72,12 +147,13 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
72
147
  primaryFailure && isThresholdWrapperMessage(outcome.error) ? primaryFailure : outcome.error || primaryFailure;
73
148
  const detail =
74
149
  status === "FAIL"
75
- ? ` ${shortenMessage(preferredFailure || "failed")}`
150
+ ? ` ${boldRed(shortenMessage(preferredFailure || "failed"))}`
76
151
  : outcome.status === "not_run"
77
- ? ` ${shortenMessage(outcome.reason || "not run")}`
152
+ ? ` ${dim(shortenMessage(outcome.reason || "not run"))}`
78
153
  : "";
154
+ const progress = mode === "compact" && totalFileCount > 0 ? `${dim(`[${completedCount}/${totalFileCount}]`)} ` : "";
79
155
  stdout.write(
80
- `${colorStatus(status)} ${task.serviceName} ${displayTaskType(task)} ${normalizePath(task.file)} ${dim(duration)}${detail}\n`
156
+ `${progress}${statusLabel(status)} ${task.serviceName} ${displayTaskType(task)} ${normalizePath(task.file)} ${dim(duration)}${detail}\n`
81
157
  );
82
158
  },
83
159
  telemetry(message) {
@@ -89,7 +165,11 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
89
165
  mode === "debug"
90
166
  ? buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation)
91
167
  : buildCompactRunSummaryLines(results, durationMs, knownFailureIssueValidation);
92
- for (const line of lines) stdout.write(`${colorSectionLine(line)}\n`);
168
+ const colored = colorFailureSummaryLines(lines.map((line) => colorSectionLine(line)));
169
+ const dimmed = colored.map((line) => dimDurations(line));
170
+ const boxed = boxLines(dimmed);
171
+ stdout.write("\n");
172
+ for (const line of boxed) stdout.write(`${line}\n`);
93
173
  },
94
174
  error(message) {
95
175
  stderr.write(`${message}\n`);
@@ -1,4 +1,5 @@
1
1
  import { Writable } from "stream";
2
+ import figures from "figures";
2
3
  import { describe, expect, it } from "vitest";
3
4
  import { createRunReporter } from "./run-reporter.mjs";
4
5
 
@@ -24,7 +25,7 @@ describe("run reporter setup output", () => {
24
25
  durationMs: 8_000,
25
26
  });
26
27
 
27
- expect(stdout).toContain("RUN SETUP api template rebuild");
28
+ expect(stdout).toContain(`${figures.play} RUN SETUP api template rebuild`);
28
29
  expect(stdout).toContain("8s");
29
30
  });
30
31
 
@@ -74,7 +75,7 @@ describe("run reporter setup output", () => {
74
75
  error: "Command failed with exit code 1: node scripts/fail-prepare.mjs",
75
76
  });
76
77
 
77
- expect(stdout).toContain("FAIL SETUP api runtime:prepare");
78
+ expect(stdout).toContain(`${figures.cross} FAIL SETUP api runtime:prepare`);
78
79
  expect(stdout).toContain("Command failed with exit code 1");
79
80
  });
80
81
  });
@@ -1,4 +1,4 @@
1
- import type { AuthAdapter, HeaderBuilder, HttpSuiteConfig } from "../index";
1
+ import type { HttpSuiteConfig, RuntimeEnv, RuntimeOptions } from "../index";
2
2
 
3
3
  export interface DatabaseTemplateConfig {
4
4
  inputs?: string[];
@@ -192,6 +192,187 @@ export interface TestkitFileMetadata {
192
192
  skip?: string | { reason: string };
193
193
  }
194
194
 
195
+ export interface ProfileRequestContext {
196
+ actor: string;
197
+ actorIndex: number;
198
+ env: RuntimeEnv;
199
+ phase: string;
200
+ }
201
+
202
+ export type ProfileValueFactory<TValue> =
203
+ | TValue
204
+ | ((context: ProfileRequestContext) => TValue);
205
+
206
+ export interface ProfileRequestConfig {
207
+ body?: ProfileValueFactory<unknown>;
208
+ contentTypeJson?: boolean;
209
+ expect?: number | number[];
210
+ headers?: ProfileValueFactory<Record<string, string>>;
211
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
212
+ path: string;
213
+ }
214
+
215
+ export interface SessionAuthSourceConfig {
216
+ key: string;
217
+ }
218
+
219
+ export interface SessionAuthHeaderConfig {
220
+ header?: string;
221
+ prefix?: string;
222
+ source: SessionAuthSourceConfig;
223
+ }
224
+
225
+ export interface SessionCaptureConfig {
226
+ auth?: SessionAuthHeaderConfig;
227
+ cookies?: Record<string, string>;
228
+ fields?: Record<string, string>;
229
+ }
230
+
231
+ export interface SessionActorConfig {
232
+ bootstrap?: ProfileRequestConfig | ProfileRequestConfig[];
233
+ login: ProfileRequestConfig;
234
+ session: SessionCaptureConfig;
235
+ }
236
+
237
+ export interface ProfileHeaderContext<TSession = unknown> {
238
+ actor: string | null;
239
+ env: RuntimeEnv;
240
+ session: TSession | null;
241
+ }
242
+
243
+ export interface ProfileSessionHeaderConfig {
244
+ actor?: string;
245
+ field: string;
246
+ header: string;
247
+ }
248
+
249
+ export interface DeterministicForwardedForConfig {
250
+ actor?: string;
251
+ header?: string;
252
+ seed?: string;
253
+ }
254
+
255
+ export interface ProfileHeaderConfig<TSession = unknown> {
256
+ contentTypeJson?: boolean;
257
+ forwardedFor?: "deterministic" | DeterministicForwardedForConfig;
258
+ fromSession?: ProfileSessionHeaderConfig[];
259
+ values?: Record<string, string> | ((context: ProfileHeaderContext<TSession>) => Record<string, string>);
260
+ }
261
+
262
+ export interface RawHttpProfileOptions {
263
+ env?: RuntimeEnv;
264
+ headers?: ProfileHeaderConfig;
265
+ options?: RuntimeOptions;
266
+ }
267
+
268
+ export interface SessionHttpProfileOptions {
269
+ actor: SessionActorConfig;
270
+ env?: RuntimeEnv;
271
+ headers?: ProfileHeaderConfig<Record<string, unknown>>;
272
+ options?: RuntimeOptions;
273
+ }
274
+
275
+ export interface MultiActorHttpProfileOptions {
276
+ actors: Record<string, SessionActorConfig>;
277
+ env?: RuntimeEnv;
278
+ headers?: ProfileHeaderConfig<Record<string, Record<string, unknown>>>;
279
+ options?: RuntimeOptions;
280
+ primaryActor?: string;
281
+ }
282
+
283
+ export interface LocalJsonIdentityContext {
284
+ actor: string;
285
+ }
286
+
287
+ export type LocalJsonIdentityValue = string | ((context: LocalJsonIdentityContext) => string);
288
+
289
+ export interface LocalJsonActorIdentityConfig {
290
+ email?: LocalJsonIdentityValue;
291
+ loginBody?: Record<string, unknown>;
292
+ name?: LocalJsonIdentityValue;
293
+ organizationName?: LocalJsonIdentityValue;
294
+ password?: LocalJsonIdentityValue;
295
+ signupBody?: Record<string, unknown>;
296
+ }
297
+
298
+ export interface LocalJsonSessionAuthOptions {
299
+ header?: string;
300
+ prefix?: string;
301
+ sourceKey?: string;
302
+ }
303
+
304
+ export interface LocalJsonSessionOptions {
305
+ auth?: LocalJsonSessionAuthOptions;
306
+ authCookie?: string;
307
+ cookies?: Record<string, string>;
308
+ fields?: Record<string, string>;
309
+ organizationIdPath?: string;
310
+ refreshCookie?: string;
311
+ }
312
+
313
+ export interface LocalJsonOrganizationHeaderConfig {
314
+ actor?: string;
315
+ field?: string;
316
+ header?: string;
317
+ }
318
+
319
+ export interface LocalJsonHeaderOptions {
320
+ contentTypeJson?: boolean;
321
+ forwardedFor?: "deterministic" | DeterministicForwardedForConfig;
322
+ organization?: false | string | LocalJsonOrganizationHeaderConfig;
323
+ values?: Record<string, string> | ((context: ProfileHeaderContext<unknown>) => Record<string, string>);
324
+ }
325
+
326
+ export interface LocalJsonSignupOptions {
327
+ enabled?: boolean;
328
+ expect?: number | number[];
329
+ path?: string;
330
+ }
331
+
332
+ export interface LocalJsonLoginOptions {
333
+ expect?: number | number[];
334
+ path?: string;
335
+ }
336
+
337
+ export interface LocalJsonProfileOptions {
338
+ env?: RuntimeEnv;
339
+ headers?: LocalJsonHeaderOptions;
340
+ identities?: Record<string, LocalJsonActorIdentityConfig>;
341
+ login?: LocalJsonLoginOptions;
342
+ options?: RuntimeOptions;
343
+ password?: LocalJsonIdentityValue;
344
+ session?: LocalJsonSessionOptions;
345
+ signup?: false | LocalJsonSignupOptions;
346
+ }
347
+
348
+ export interface LocalJsonSessionProfileOptions {
349
+ actor?: string;
350
+ env?: RuntimeEnv;
351
+ headers?: LocalJsonHeaderOptions;
352
+ identity?: LocalJsonActorIdentityConfig;
353
+ options?: RuntimeOptions;
354
+ }
355
+
356
+ export interface LocalJsonMultiActorProfileOptions {
357
+ actors?: string[] | Record<string, LocalJsonActorIdentityConfig>;
358
+ env?: RuntimeEnv;
359
+ headers?: LocalJsonHeaderOptions;
360
+ options?: RuntimeOptions;
361
+ primaryActor?: string;
362
+ }
363
+
364
+ export interface LocalJsonRawProfileOptions {
365
+ env?: RuntimeEnv;
366
+ headers?: LocalJsonHeaderOptions;
367
+ options?: RuntimeOptions;
368
+ }
369
+
370
+ export interface LocalJsonProfileBuilder {
371
+ multiActor(options?: LocalJsonMultiActorProfileOptions): HttpSuiteConfig<Record<string, Record<string, unknown>>>;
372
+ raw(options?: LocalJsonRawProfileOptions): HttpSuiteConfig<any>;
373
+ session(options?: LocalJsonSessionProfileOptions): HttpSuiteConfig<Record<string, unknown>>;
374
+ }
375
+
195
376
  export interface NodeAppOptions extends Omit<ServiceConfig, "local" | "runtime" | "env"> {
196
377
  baseUrl?: string;
197
378
  build?: BuildConfig | null;
@@ -246,7 +427,6 @@ export interface TestkitConfig {
246
427
  }
247
428
 
248
429
  export declare function defineConfig<T extends TestkitConfig>(config: T): T;
249
- export declare function defineHttpProfile<T extends HttpSuiteConfig>(profile: T): T;
250
430
  export declare function defineFile<T extends TestkitFileMetadata>(metadata: T): T;
251
431
  export declare const app: {
252
432
  node(options: NodeAppOptions): ServiceConfig;
@@ -269,20 +449,13 @@ export declare const database: {
269
449
  export declare const toolchain: {
270
450
  node(options?: NodeToolchainConfig): NodeToolchainConfig;
271
451
  };
272
- export declare function clerkSessionProfile(options?: {
273
- apiBase?: string;
274
- needsAuth?: boolean;
275
- secretKeyEnv?: string;
276
- }): HttpSuiteConfig;
277
- export declare function jsonSessionProfile(options?: {
278
- body?: (context: { env: Record<string, string> }) => unknown;
279
- cookieName?: string;
280
- headers?: Record<string, string>;
281
- loginPath?: string;
282
- passwordEnv?: string;
283
- successStatus?: number;
284
- usernameEnv?: string;
285
- }): HttpSuiteConfig;
452
+ export declare const profiles: {
453
+ custom<T extends HttpSuiteConfig>(profile: T): T;
454
+ localJson(options?: LocalJsonProfileOptions): LocalJsonProfileBuilder;
455
+ multiActor(options: MultiActorHttpProfileOptions): HttpSuiteConfig<Record<string, Record<string, unknown>>>;
456
+ raw(options?: RawHttpProfileOptions): HttpSuiteConfig<any>;
457
+ session(options: SessionHttpProfileOptions): HttpSuiteConfig<Record<string, unknown>>;
458
+ };
286
459
 
287
460
  export declare function registerRepoConfig(config: unknown): void;
288
461
  export declare function getRepoConfig(): unknown;