@elench/testkit 0.1.52 → 0.1.54

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 (52) hide show
  1. package/README.md +14 -0
  2. package/bin/testkit.mjs +4 -6
  3. package/lib/cli/command-helpers.mjs +170 -0
  4. package/lib/cli/commands/artifacts.mjs +45 -0
  5. package/lib/cli/commands/cleanup.mjs +15 -0
  6. package/lib/cli/commands/db/snapshot/capture.mjs +22 -0
  7. package/lib/cli/commands/destroy.mjs +15 -0
  8. package/lib/cli/commands/known-failures/render.mjs +19 -0
  9. package/lib/cli/commands/known-failures/validate.mjs +20 -0
  10. package/lib/cli/commands/logs.mjs +47 -0
  11. package/lib/cli/commands/run.mjs +23 -0
  12. package/lib/cli/commands/show.mjs +47 -0
  13. package/lib/cli/commands/status.mjs +15 -0
  14. package/lib/cli/commands/watch.mjs +23 -0
  15. package/lib/cli/entrypoint.mjs +83 -0
  16. package/lib/cli/index.mjs +6 -116
  17. package/lib/cli/presentation/code-frames.mjs +57 -0
  18. package/lib/cli/presentation/code-frames.test.mjs +71 -0
  19. package/lib/cli/presentation/colors.mjs +29 -0
  20. package/lib/cli/presentation/run-reporter.mjs +100 -0
  21. package/lib/cli/tui/watch-app.mjs +104 -0
  22. package/lib/cli/viewer.mjs +268 -0
  23. package/lib/known-failures/index.mjs +1 -1
  24. package/lib/known-failures/index.test.mjs +46 -0
  25. package/lib/runner/artifacts.mjs +35 -0
  26. package/lib/runner/default-runtime-errors.mjs +66 -0
  27. package/lib/runner/default-runtime-runner.mjs +52 -11
  28. package/lib/runner/failure-details.mjs +31 -0
  29. package/lib/runner/failure-details.test.mjs +51 -0
  30. package/lib/runner/formatting.mjs +207 -0
  31. package/lib/runner/formatting.test.mjs +81 -6
  32. package/lib/runner/logs.mjs +89 -0
  33. package/lib/runner/orchestrator.mjs +51 -20
  34. package/lib/runner/playwright-runner.mjs +15 -7
  35. package/lib/runner/processes.mjs +9 -11
  36. package/lib/runner/reporting.mjs +5 -1
  37. package/lib/runner/reporting.test.mjs +4 -1
  38. package/lib/runner/runtime-contexts.mjs +7 -3
  39. package/lib/runner/runtime-manager.mjs +8 -2
  40. package/lib/runner/runtime-preparation.mjs +9 -4
  41. package/lib/runner/services.mjs +25 -8
  42. package/lib/runner/template-steps.mjs +4 -3
  43. package/lib/runner/triage.mjs +67 -0
  44. package/lib/runner/worker-loop.mjs +8 -7
  45. package/lib/runtime/index.d.ts +60 -0
  46. package/lib/runtime/index.mjs +12 -0
  47. package/lib/runtime-src/k6/checks.js +45 -12
  48. package/lib/runtime-src/k6/http-assertions.js +214 -0
  49. package/lib/runtime-src/k6/http.js +261 -13
  50. package/lib/runtime-src/k6/suite.js +46 -1
  51. package/lib/toolchains/index.mjs +6 -3
  52. package/package.json +13 -3
@@ -55,9 +55,12 @@ export function killChildProcess(child, signal) {
55
55
  }
56
56
  }
57
57
 
58
- export function pipeOutput(stream, prefix) {
58
+ export function captureOutput(stream, options = {}) {
59
59
  if (!stream) return Promise.resolve();
60
60
 
61
+ const onLine = typeof options.onLine === "function" ? options.onLine : null;
62
+ const liveWriter = typeof options.liveWriter === "function" ? options.liveWriter : null;
63
+ const livePrefix = options.livePrefix || "";
61
64
  let pending = "";
62
65
  return new Promise((resolve) => {
63
66
  let settled = false;
@@ -72,12 +75,15 @@ export function pipeOutput(stream, prefix) {
72
75
  const lines = pending.split(/\r?\n/);
73
76
  pending = lines.pop() || "";
74
77
  for (const line of lines) {
75
- if (line.length > 0) console.log(`${prefix} ${line}`);
78
+ if (line.length === 0) continue;
79
+ onLine?.(line);
80
+ if (liveWriter) liveWriter(livePrefix ? `${livePrefix} ${line}` : line);
76
81
  }
77
82
  });
78
83
  stream.on("end", () => {
79
84
  if (pending.length > 0) {
80
- console.log(`${prefix} ${pending}`);
85
+ onLine?.(pending);
86
+ if (liveWriter) liveWriter(livePrefix ? `${livePrefix} ${pending}` : pending);
81
87
  }
82
88
  settle();
83
89
  });
@@ -86,14 +92,6 @@ export function pipeOutput(stream, prefix) {
86
92
  });
87
93
  }
88
94
 
89
- export function printBufferedOutput(output, prefix) {
90
- for (const line of output.split(/\r?\n/)) {
91
- if (line.trim().length > 0) {
92
- console.log(`${prefix} ${line}`);
93
- }
94
- }
95
- }
96
-
97
95
  export async function stopChildProcess(child, outputDrains = []) {
98
96
  if (!child) return;
99
97
  if (child.exitCode !== null) {
@@ -111,6 +111,7 @@ export function buildRunArtifact({
111
111
  serviceFilter,
112
112
  metadata,
113
113
  summarizeDbBackend,
114
+ serviceLogs = [],
114
115
  }) {
115
116
  const executed = results.filter((result) => !result.skipped);
116
117
  const effectivelySkippedServices = executed.filter(isEffectivelySkippedService);
@@ -127,7 +128,7 @@ export function buildRunArtifact({
127
128
  const dbBackend = summarizeDbBackend(results);
128
129
 
129
130
  return {
130
- schemaVersion: 6,
131
+ schemaVersion: 7,
131
132
  source: "testkit",
132
133
  generatedAt: new Date(finishedAt).toISOString(),
133
134
  product: {
@@ -176,6 +177,9 @@ export function buildRunArtifact({
176
177
  notRun: notRunFiles,
177
178
  },
178
179
  },
180
+ logs: {
181
+ services: serviceLogs,
182
+ },
179
183
  services: results.map((result) => ({
180
184
  name: result.name,
181
185
  failed: result.failed,
@@ -78,7 +78,7 @@ describe("runner reporting", () => {
78
78
  });
79
79
 
80
80
  expect(artifact.product.name).toBe("my-product");
81
- expect(artifact.schemaVersion).toBe(6);
81
+ expect(artifact.schemaVersion).toBe(7);
82
82
  expect(artifact.run).toMatchObject({
83
83
  workers: 2,
84
84
  fileTimeoutSeconds: 60,
@@ -107,6 +107,9 @@ describe("runner reporting", () => {
107
107
  });
108
108
  expect(artifact.services[0].durationMs).toBe(1200);
109
109
  expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
110
+ expect(artifact.logs).toEqual({
111
+ services: [],
112
+ });
110
113
  });
111
114
 
112
115
  it("builds deterministic status artifacts", () => {
@@ -35,12 +35,12 @@ export function createRuntimeInstanceContext(runtimeId, graph, productDir) {
35
35
  };
36
36
  }
37
37
 
38
- export async function ensureRuntimeInstanceReady(context, task, lifecycle) {
38
+ export async function ensureRuntimeInstanceReady(context, task, lifecycle, options = {}) {
39
39
  if (!context.prepared) {
40
40
  if (!context.preparationPromise) {
41
41
  context.preparationPromise = (async () => {
42
42
  await prepareDatabases(context.runtimeConfigs);
43
- await prepareRuntimeServices(context.runtimeConfigs);
43
+ await prepareRuntimeServices(context.runtimeConfigs, options);
44
44
  context.prepared = true;
45
45
  })().finally(() => {
46
46
  context.preparationPromise = null;
@@ -52,7 +52,11 @@ export async function ensureRuntimeInstanceReady(context, task, lifecycle) {
52
52
  if (taskNeedsLocalRuntime(task) && !context.started) {
53
53
  if (!context.startupPromise) {
54
54
  context.startupPromise = (async () => {
55
- context.startedServices = await startLocalServices(context.runtimeConfigs, lifecycle);
55
+ context.startedServices = await startLocalServices(
56
+ context.runtimeConfigs,
57
+ lifecycle,
58
+ options
59
+ );
56
60
  context.started = true;
57
61
  })().finally(() => {
58
62
  context.startupPromise = null;
@@ -8,7 +8,7 @@ import {
8
8
  ensureRuntimeInstanceReady,
9
9
  } from "./runtime-contexts.mjs";
10
10
 
11
- export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {} }) {
11
+ export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {}, runtimeOptions = {} }) {
12
12
  const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
13
13
  const pools = new Map();
14
14
  const locks = new Map();
@@ -19,6 +19,7 @@ export function createRuntimeManager({ productDir, graphs, lifecycle, hooks = {}
19
19
  createRuntimeInstanceContext,
20
20
  ensureRuntimeInstanceReady,
21
21
  sleep,
22
+ options: runtimeOptions,
22
23
  ...hooks,
23
24
  };
24
25
 
@@ -199,7 +200,12 @@ async function getReadyContext(slot, productDir, task, lifecycle, runtimeHooks)
199
200
  slot.contextPromise = Promise.resolve(slot.context);
200
201
  }
201
202
  await slot.contextPromise;
202
- await runtimeHooks.ensureRuntimeInstanceReady(slot.context, task, lifecycle);
203
+ await runtimeHooks.ensureRuntimeInstanceReady(
204
+ slot.context,
205
+ task,
206
+ lifecycle,
207
+ runtimeHooks.options || {}
208
+ );
203
209
  return slot.context;
204
210
  }
205
211
 
@@ -13,13 +13,13 @@ import {
13
13
 
14
14
  const MANIFEST_FILE = "prepare-manifest.json";
15
15
 
16
- export async function prepareRuntimeServices(runtimeConfigs) {
16
+ export async function prepareRuntimeServices(runtimeConfigs, options = {}) {
17
17
  for (const config of runtimeConfigs) {
18
- await prepareRuntimeService(config);
18
+ await prepareRuntimeService(config, options);
19
19
  }
20
20
  }
21
21
 
22
- export async function prepareRuntimeService(config) {
22
+ export async function prepareRuntimeService(config, options = {}) {
23
23
  const prepare = config.testkit.runtime.prepare;
24
24
  if (!prepare || prepare.steps.length === 0) return;
25
25
 
@@ -43,12 +43,17 @@ export async function prepareRuntimeService(config) {
43
43
  }
44
44
 
45
45
  try {
46
- await announceResolvedToolchain(config, await resolveConfiguredToolchain(config));
46
+ await announceResolvedToolchain(
47
+ config,
48
+ await resolveConfiguredToolchain(config),
49
+ options.reporter
50
+ );
47
51
  await runConfiguredSteps({
48
52
  config,
49
53
  steps: prepare.steps,
50
54
  env,
51
55
  labelPrefix: "runtime:prepare",
56
+ reporter: options.reporter,
52
57
  });
53
58
  writePrepareManifest(manifestPath, {
54
59
  fingerprint,
@@ -6,16 +6,16 @@ import {
6
6
  } from "../toolchains/index.mjs";
7
7
  import { buildExecutionEnv, numericPortFromUrl } from "./template.mjs";
8
8
  import { DEFAULT_READY_TIMEOUT_MS, assertLocalServicePortsAvailable, isPortInUse, waitForReady } from "./readiness.mjs";
9
- import { killChildProcess, pipeOutput, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
9
+ import { captureOutput, killChildProcess, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
10
10
  import { readDatabaseUrl } from "./state-io.mjs";
11
11
 
12
- export async function startLocalServices(runtimeConfigs, lifecycle) {
12
+ export async function startLocalServices(runtimeConfigs, lifecycle, options = {}) {
13
13
  const started = [];
14
14
 
15
15
  try {
16
16
  for (const config of runtimeConfigs) {
17
17
  if (!config.testkit.local) continue;
18
- const proc = await startLocalService(config, lifecycle);
18
+ const proc = await startLocalService(config, lifecycle, options);
19
19
  started.push(proc);
20
20
  }
21
21
  } catch (error) {
@@ -26,10 +26,10 @@ export async function startLocalServices(runtimeConfigs, lifecycle) {
26
26
  return started;
27
27
  }
28
28
 
29
- export async function startLocalService(config, lifecycle) {
29
+ export async function startLocalService(config, lifecycle, options = {}) {
30
30
  const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
31
31
  const resolvedToolchain = await resolveConfiguredToolchain(config);
32
- await announceResolvedToolchain(config, resolvedToolchain);
32
+ await announceResolvedToolchain(config, resolvedToolchain, options.reporter);
33
33
  const env = applyToolchainEnv(
34
34
  buildExecutionEnv(config, config.testkit.local.env, process.env),
35
35
  resolvedToolchain
@@ -46,12 +46,29 @@ export async function startLocalService(config, lifecycle) {
46
46
 
47
47
  await assertLocalServicePortsAvailable(config, isPortInUse);
48
48
 
49
- console.log(`Starting ${config.runtimeLabel}:${config.name}: ${config.testkit.local.start}`);
49
+ options.reporter?.localServiceStarting?.(config, config.testkit.local.start);
50
50
  const child = startDetachedCommand(config.testkit.local.start, cwd, env);
51
+ const logRecord = options.logRegistry?.ensureServiceLogRecord(config);
52
+ const liveWriter =
53
+ options.reporter?.outputMode === "debug"
54
+ ? (line) => options.reporter.writeDebugLine?.(line)
55
+ : null;
51
56
 
52
57
  const outputDrains = [
53
- pipeOutput(child.stdout, `[${config.runtimeLabel}:${config.name}]`),
54
- pipeOutput(child.stderr, `[${config.runtimeLabel}:${config.name}]`),
58
+ captureOutput(child.stdout, {
59
+ livePrefix: `[${config.runtimeLabel}:${config.name}]`,
60
+ liveWriter,
61
+ onLine(line) {
62
+ if (logRecord) options.logRegistry.append(logRecord, "stdout", line);
63
+ },
64
+ }),
65
+ captureOutput(child.stderr, {
66
+ livePrefix: `[${config.runtimeLabel}:${config.name}]`,
67
+ liveWriter,
68
+ onLine(line) {
69
+ if (logRecord) options.logRegistry.append(logRecord, "stderr", line);
70
+ },
71
+ }),
55
72
  ];
56
73
  lifecycle.registerService(config, child, cwd, () => {
57
74
  killChildProcess(child, "SIGTERM");
@@ -23,14 +23,15 @@ const MODULE_RUNNER_ENTRY = path.join(
23
23
  "template-step-module-runner.mjs"
24
24
  );
25
25
 
26
- export async function runConfiguredSteps({ config, steps = [], env, labelPrefix }) {
26
+ export async function runConfiguredSteps({ config, steps = [], env, labelPrefix, reporter = null }) {
27
27
  if (steps.length === 0) return;
28
28
  const resolvedToolchain = await resolveConfiguredToolchain(config);
29
- await announceResolvedToolchain(config, resolvedToolchain);
29
+ await announceResolvedToolchain(config, resolvedToolchain, reporter);
30
30
 
31
31
  for (const [index, step] of steps.entries()) {
32
32
  const label = `${labelPrefix}:${config.name}:${index + 1}`;
33
- console.log(`\n── ${label} ──`);
33
+ if (reporter?.phaseStarted) reporter.phaseStarted(label);
34
+ else console.log(`\n── ${label} ──`);
34
35
  await runConfiguredStep(config, step, env, resolvedToolchain);
35
36
  }
36
37
  }
@@ -80,6 +80,49 @@ export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, known
80
80
  return { runArtifact, statusArtifact };
81
81
  }
82
82
 
83
+ export function applyKnownFailureIssueValidationToArtifacts(
84
+ runArtifact,
85
+ statusArtifact,
86
+ issueValidation
87
+ ) {
88
+ if (!issueValidation) return { runArtifact, statusArtifact };
89
+
90
+ const validationById = new Map(
91
+ (issueValidation.entries || []).map((entry) => [entry.id, entry])
92
+ );
93
+
94
+ for (const entry of [...extractRunFileEntries(runArtifact), ...extractStatusFileEntries(statusArtifact)]) {
95
+ const triage = entry.target ? entry.target.triage : entry.triage;
96
+ if (!triage?.entries?.length) continue;
97
+
98
+ const matchedValidationEntries = triage.entries
99
+ .map((triageEntry) => validationById.get(triageEntry.id))
100
+ .filter(Boolean);
101
+
102
+ if (matchedValidationEntries.length === 0) continue;
103
+
104
+ const enrichedEntries = triage.entries.map((triageEntry) => {
105
+ const validationEntry = validationById.get(triageEntry.id);
106
+ if (!validationEntry) return triageEntry;
107
+ return {
108
+ ...triageEntry,
109
+ github: validationEntry.github,
110
+ validationStatus: validationEntry.status,
111
+ findings: validationEntry.findings,
112
+ };
113
+ });
114
+
115
+ const nextTriage = {
116
+ ...triage,
117
+ entries: enrichedEntries,
118
+ availability: summarizeIssueValidationAvailability(issueValidation, matchedValidationEntries),
119
+ };
120
+ setEntryTriage(entry, nextTriage);
121
+ }
122
+
123
+ return { runArtifact, statusArtifact };
124
+ }
125
+
83
126
  function toArtifactTriageEntry(entry) {
84
127
  return {
85
128
  id: entry.id,
@@ -152,3 +195,27 @@ function setEntryTriage(entry, triage) {
152
195
  }
153
196
  entry.triage = triage;
154
197
  }
198
+
199
+ function summarizeIssueValidationAvailability(issueValidation, entries) {
200
+ if (entries.some((entry) => entry.github?.cached)) {
201
+ return {
202
+ mode: "cache",
203
+ reason: issueValidation.availability?.usedCachedFallback
204
+ ? "used stale cache"
205
+ : "used cached issue metadata",
206
+ };
207
+ }
208
+ if (entries.some((entry) => entry.status === "validation_unavailable")) {
209
+ const globalReason = (issueValidation.findings || []).find(
210
+ (finding) => finding.code === "validation_unavailable"
211
+ );
212
+ return {
213
+ mode: "offline",
214
+ reason: globalReason?.message || "validation unavailable",
215
+ };
216
+ }
217
+ return {
218
+ mode: "live",
219
+ reason: null,
220
+ };
221
+ }
@@ -23,10 +23,10 @@ export async function runWorker(
23
23
  lifecycle,
24
24
  claimNextTask,
25
25
  recordTaskOutcome,
26
- recordGraphError
26
+ recordGraphError,
27
+ reporter = null
27
28
  ) {
28
29
  const startedAt = Date.now();
29
- console.log(`\n══ worker ${worker.workerId} ══`);
30
30
  const errors = [];
31
31
 
32
32
  try {
@@ -69,8 +69,9 @@ export async function runWorker(
69
69
  }
70
70
  worker.currentGraphKey = task.graphKey;
71
71
  lease = await runtimeManager.acquire(task);
72
- const outcome = await runTask(lease.context, task, lifecycle, lease);
72
+ const outcome = await runTask(lease.context, task, lifecycle, lease, reporter);
73
73
  recordTaskOutcome(trackers, outcome.task, outcome);
74
+ reporter?.taskFinished?.(outcome.task, outcome);
74
75
  timingUpdates.push({
75
76
  key: outcome.task.timingKey,
76
77
  durationMs: outcome.durationMs,
@@ -100,20 +101,20 @@ export async function runWorker(
100
101
  };
101
102
  }
102
103
 
103
- async function runTask(context, task, lifecycle, lease) {
104
+ async function runTask(context, task, lifecycle, lease, reporter = null) {
104
105
  const targetConfig = context.configByName.get(task.targetName);
105
106
  if (!targetConfig) {
106
107
  throw new Error(`Runtime instance is missing target config "${task.targetName}"`);
107
108
  }
108
109
 
109
110
  if (task.framework === "playwright") {
110
- return runPlaywrightTask(targetConfig, task, lifecycle, lease);
111
+ return runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter);
111
112
  }
112
113
  if (task.type === "dal") {
113
- return runDalTask(targetConfig, task, lifecycle, lease);
114
+ return runDalTask(targetConfig, task, lifecycle, lease, reporter);
114
115
  }
115
116
  if (task.framework === "k6" && HTTP_K6_TYPES.has(task.type)) {
116
- return runHttpK6Task(targetConfig, task, lifecycle, lease);
117
+ return runHttpK6Task(targetConfig, task, lifecycle, lease, reporter);
117
118
  }
118
119
 
119
120
  throw new Error(
@@ -9,6 +9,9 @@ export interface RuntimeCookie {
9
9
  }
10
10
 
11
11
  export interface RuntimeResponse {
12
+ __testkit?: {
13
+ httpTrace?: RuntimeHttpTrace;
14
+ };
12
15
  body: string;
13
16
  cookies?: Record<string, RuntimeCookie[]>;
14
17
  headers?: Record<string, string | string[] | undefined>;
@@ -18,6 +21,24 @@ export interface RuntimeResponse {
18
21
  };
19
22
  }
20
23
 
24
+ export interface RuntimeHttpTrace {
25
+ id: string;
26
+ requestId: string;
27
+ startedAt: string;
28
+ finishedAt?: string;
29
+ durationMs?: number;
30
+ method: RuntimeMethod;
31
+ path: string;
32
+ url: string;
33
+ requestHeaders: Record<string, string | string[] | null>;
34
+ response?: {
35
+ status: number | null;
36
+ contentType: string | string[] | null;
37
+ bodyPreview: string | null;
38
+ bodyTruncated: boolean;
39
+ };
40
+ }
41
+
21
42
  export interface RuntimeOptions {
22
43
  [key: string]: unknown;
23
44
  thresholds?: Record<string, unknown>;
@@ -144,6 +165,12 @@ export declare const check: <T>(
144
165
  value: T,
145
166
  checks: Record<string, (value: T) => boolean>
146
167
  ) => boolean;
168
+ export declare const evaluateCheck: <T>(
169
+ value: T,
170
+ checkName: string,
171
+ predicate: (value: T) => boolean,
172
+ detailFactory?: (() => Record<string, unknown>) | null
173
+ ) => boolean;
147
174
  export declare const fail: (message: string) => never;
148
175
  export declare const group: (name: string, fn: () => void) => void;
149
176
  export declare const sleep: (seconds?: number) => void;
@@ -152,6 +179,9 @@ export declare const http: RuntimeHttpClient;
152
179
 
153
180
  export declare function file(data: unknown, filename?: string, contentType?: string): unknown;
154
181
  export declare function json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
182
+ export declare function safeJson<T = unknown>(
183
+ response: Pick<RuntimeResponse, "body">
184
+ ): { ok: true; value: T } | { ok: false; error: string };
155
185
  export declare function remainingTimeSeconds(): number;
156
186
  export declare function waitFor<T>(
157
187
  read: () => T,
@@ -188,6 +218,9 @@ export declare function truncate(db: RuntimeDb, ...tables: string[]): void;
188
218
  export declare function getTestkitContext(): TestkitRuntimeContext;
189
219
 
190
220
  export declare function getEnv(): RuntimeEnv;
221
+ export declare function getHttpTrace(response: RuntimeResponse): RuntimeHttpTrace | null;
222
+ export declare function summarizeHttpTrace(response: RuntimeResponse): RuntimeHttpTrace | null;
223
+ export declare function toBodyPreview(response: RuntimeResponse): string | null;
191
224
  export declare function createHttpClient<TSetup = unknown>(
192
225
  config: HttpClientConfig<TSetup>
193
226
  ): HttpClient<TSetup>;
@@ -207,6 +240,33 @@ export declare function makeGetWithHeaders<TSetup = unknown>(
207
240
  getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
208
241
  ): HttpClient<TSetup>["getWithHeaders"];
209
242
 
243
+ export declare function expectStatus(
244
+ response: RuntimeResponse,
245
+ expected: number,
246
+ label?: string | null
247
+ ): boolean;
248
+ export declare function expectStatusOneOf(
249
+ response: RuntimeResponse,
250
+ expectedValues: number[],
251
+ label?: string | null
252
+ ): boolean;
253
+ export declare function expectNotStatus(
254
+ response: RuntimeResponse,
255
+ unexpected: number,
256
+ label?: string | null
257
+ ): boolean;
258
+ export declare function expectJson<T = unknown>(
259
+ response: RuntimeResponse,
260
+ predicate: (value: T) => boolean,
261
+ label?: string | null
262
+ ): boolean;
263
+ export declare function expectJsonPath(
264
+ response: RuntimeResponse,
265
+ path: string,
266
+ predicate: (value: unknown) => boolean,
267
+ label?: string | null
268
+ ): boolean;
269
+
210
270
  declare global {
211
271
  const __ENV: Record<string, string | undefined>;
212
272
  }
@@ -26,10 +26,18 @@ export {
26
26
  allMatch,
27
27
  contains,
28
28
  defaultOptions,
29
+ evaluateCheck,
29
30
  isSorted,
30
31
  json,
31
32
  singleIterationOptions,
32
33
  } from "../runtime-src/k6/checks.js";
34
+ export {
35
+ expectJson,
36
+ expectJsonPath,
37
+ expectNotStatus,
38
+ expectStatus,
39
+ expectStatusOneOf,
40
+ } from "../runtime-src/k6/http-assertions.js";
33
41
  export {
34
42
  createDalContext,
35
43
  openDb,
@@ -39,9 +47,13 @@ export {
39
47
  createHttpClient,
40
48
  defaultOptions as httpDefaultOptions,
41
49
  getEnv,
50
+ getHttpTrace,
42
51
  makeGetWithHeaders,
43
52
  makeRawReq,
44
53
  makeReq,
54
+ safeJson,
55
+ summarizeHttpTrace,
56
+ toBodyPreview,
45
57
  } from "../runtime-src/k6/http.js";
46
58
 
47
59
  export function getTestkitContext() {
@@ -24,23 +24,33 @@ export function check(value, checks) {
24
24
 
25
25
  for (const [name, predicate] of Object.entries(checks || {})) {
26
26
  const checkName = normalizeLabel(name, "unnamed check");
27
- const passed = k6Check(value, { [checkName]: predicate });
28
- if (!passed) {
29
- recordFailureDetail({
30
- kind: "k6-check",
31
- key: buildFailureKey(failureState.groupStack, checkName),
32
- title: checkName,
33
- checkName,
34
- groupPath: [...failureState.groupStack],
35
- phase: failureState.phase,
36
- });
37
- allPassed = false;
38
- }
27
+ const passed = evaluateCheck(value, checkName, predicate);
28
+ if (!passed) allPassed = false;
39
29
  }
40
30
 
41
31
  return allPassed;
42
32
  }
43
33
 
34
+ export function evaluateCheck(value, checkName, predicate, detailFactory = null) {
35
+ const normalizedName = normalizeLabel(checkName, "unnamed check");
36
+ const passed = k6Check(value, { [normalizedName]: predicate });
37
+ if (!passed) {
38
+ recordFailureDetail(
39
+ typeof detailFactory === "function"
40
+ ? detailFactory()
41
+ : {
42
+ kind: "k6-check",
43
+ key: buildFailureKey(failureState.groupStack, normalizedName),
44
+ title: normalizedName,
45
+ checkName: normalizedName,
46
+ groupPath: [...failureState.groupStack],
47
+ phase: failureState.phase,
48
+ }
49
+ );
50
+ }
51
+ return passed;
52
+ }
53
+
44
54
  export function group(name, fn) {
45
55
  const groupName = normalizeLabel(name, "unnamed group");
46
56
 
@@ -158,6 +168,24 @@ function normalizeFailureDetail(detail) {
158
168
  const message = normalizeLabel(detail.message, null);
159
169
  if (message) normalized.message = message;
160
170
 
171
+ if (detail.expected !== undefined) normalized.expected = detail.expected;
172
+ if (detail.actual !== undefined) normalized.actual = detail.actual;
173
+
174
+ const traceId = normalizeLabel(detail.traceId, null);
175
+ if (traceId) normalized.traceId = traceId;
176
+
177
+ const request = normalizeObject(detail.request);
178
+ if (request) normalized.request = request;
179
+
180
+ const response = normalizeObject(detail.response);
181
+ if (response) normalized.response = response;
182
+
183
+ const location = normalizeObject(detail.location);
184
+ if (location) normalized.location = location;
185
+
186
+ const stack = normalizeLabel(detail.stack, null);
187
+ if (stack) normalized.stack = stack;
188
+
161
189
  const groupPath = Array.isArray(detail.groupPath)
162
190
  ? detail.groupPath.map((entry) => normalizeLabel(entry, null)).filter(Boolean)
163
191
  : [];
@@ -176,3 +204,8 @@ function normalizeLabel(value, fallback) {
176
204
  const normalized = value.trim();
177
205
  return normalized.length > 0 ? normalized : fallback;
178
206
  }
207
+
208
+ function normalizeObject(value) {
209
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
210
+ return JSON.parse(JSON.stringify(value));
211
+ }