@elench/testkit 0.1.53 → 0.1.55

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 (41) hide show
  1. package/lib/cli/commands/artifacts.mjs +2 -2
  2. package/lib/cli/commands/logs.mjs +2 -2
  3. package/lib/cli/commands/show.mjs +2 -2
  4. package/lib/cli/db.mjs +17 -2
  5. package/lib/cli/presentation/code-frames.mjs +57 -0
  6. package/lib/cli/presentation/code-frames.test.mjs +71 -0
  7. package/lib/cli/presentation/colors.mjs +29 -0
  8. package/lib/cli/presentation/run-reporter.mjs +41 -7
  9. package/lib/cli/presentation/run-reporter.test.mjs +80 -0
  10. package/lib/cli/tui/watch-app.mjs +134 -18
  11. package/lib/cli/viewer.mjs +146 -4
  12. package/lib/database/index.mjs +85 -11
  13. package/lib/database/template-steps.mjs +45 -6
  14. package/lib/database/template-steps.test.mjs +43 -0
  15. package/lib/known-failures/index.mjs +1 -1
  16. package/lib/known-failures/index.test.mjs +46 -0
  17. package/lib/runner/artifacts.mjs +16 -0
  18. package/lib/runner/default-runtime-errors.mjs +66 -0
  19. package/lib/runner/default-runtime-runner.mjs +8 -1
  20. package/lib/runner/failure-details.mjs +31 -0
  21. package/lib/runner/failure-details.test.mjs +51 -0
  22. package/lib/runner/formatting.mjs +114 -4
  23. package/lib/runner/formatting.test.mjs +77 -0
  24. package/lib/runner/logs.mjs +71 -6
  25. package/lib/runner/orchestrator.mjs +63 -7
  26. package/lib/runner/reporting.mjs +52 -2
  27. package/lib/runner/reporting.test.mjs +80 -2
  28. package/lib/runner/runtime-contexts.mjs +3 -3
  29. package/lib/runner/runtime-preparation.mjs +31 -0
  30. package/lib/runner/setup-operations.mjs +115 -0
  31. package/lib/runner/setup-operations.test.mjs +94 -0
  32. package/lib/runner/template-steps.mjs +129 -11
  33. package/lib/runner/triage.mjs +67 -0
  34. package/lib/runtime/index.d.ts +60 -0
  35. package/lib/runtime/index.mjs +12 -0
  36. package/lib/runtime-src/k6/checks.js +45 -12
  37. package/lib/runtime-src/k6/http-assertions.js +214 -0
  38. package/lib/runtime-src/k6/http.js +261 -13
  39. package/lib/runtime-src/k6/suite.js +46 -1
  40. package/lib/toolchains/index.mjs +0 -4
  41. package/package.json +3 -1
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,214 @@
1
+ import { evaluateCheck, recordFailureDetail } from "./checks.js";
2
+ import { safeJson, summarizeHttpTrace, toBodyPreview } from "./http.js";
3
+
4
+ export function expectStatus(response, expected, label = null) {
5
+ return expectStatusOneOf(response, [expected], label);
6
+ }
7
+
8
+ export function expectStatusOneOf(response, expectedValues, label = null) {
9
+ const values = normalizeExpectedValues(expectedValues);
10
+ const title = normalizeLabel(label, buildExpectedStatusTitle(values));
11
+ const trace = summarizeHttpTrace(response);
12
+ const actual = Number(response?.status ?? 0);
13
+
14
+ return evaluateCheck(
15
+ response,
16
+ title,
17
+ (res) => values.includes(Number(res?.status ?? 0)),
18
+ () => buildHttpAssertionDetail({
19
+ kind: "http-assertion",
20
+ title,
21
+ trace,
22
+ expected: values.length === 1 ? values[0] : values,
23
+ actual,
24
+ response,
25
+ message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} expected ${formatExpectedValues(values)}, got ${actual}`,
26
+ })
27
+ );
28
+ }
29
+
30
+ export function expectNotStatus(response, unexpected, label = null) {
31
+ const title = normalizeLabel(label, `status is not ${unexpected}`);
32
+ const trace = summarizeHttpTrace(response);
33
+ const actual = Number(response?.status ?? 0);
34
+
35
+ return evaluateCheck(
36
+ response,
37
+ title,
38
+ (res) => Number(res?.status ?? 0) !== unexpected,
39
+ () => buildHttpAssertionDetail({
40
+ kind: "http-assertion",
41
+ title,
42
+ trace,
43
+ expected: { not: unexpected },
44
+ actual,
45
+ response,
46
+ message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} expected not ${unexpected}, got ${actual}`,
47
+ })
48
+ );
49
+ }
50
+
51
+ export function expectJson(response, predicate, label = null) {
52
+ const title = normalizeLabel(label, "response JSON matches expectation");
53
+ const trace = summarizeHttpTrace(response);
54
+ const jsonResult = safeJson(response);
55
+
56
+ if (!jsonResult.ok) {
57
+ recordFailureDetail(
58
+ buildHttpAssertionDetail({
59
+ kind: "http-json-assertion",
60
+ title,
61
+ trace,
62
+ expected: "valid JSON response",
63
+ actual: "invalid JSON response",
64
+ response,
65
+ message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} did not return valid JSON`,
66
+ })
67
+ );
68
+ return false;
69
+ }
70
+
71
+ return evaluateCheck(
72
+ jsonResult.value,
73
+ title,
74
+ (value) => Boolean(predicate(value)),
75
+ () => ({
76
+ kind: "json-assertion",
77
+ key: buildTraceFailureKey(trace, title),
78
+ title,
79
+ message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} JSON assertion failed`,
80
+ request: trace ? buildRequestSummary(trace) : null,
81
+ response: trace ? buildResponseSummary(trace) : null,
82
+ actual: toValuePreview(jsonResult.value),
83
+ })
84
+ );
85
+ }
86
+
87
+ export function expectJsonPath(response, path, predicate, label = null) {
88
+ const title = normalizeLabel(label, `response JSON path ${path} matches expectation`);
89
+ const trace = summarizeHttpTrace(response);
90
+ const jsonResult = safeJson(response);
91
+
92
+ if (!jsonResult.ok) {
93
+ recordFailureDetail(
94
+ buildHttpAssertionDetail({
95
+ kind: "http-json-assertion",
96
+ title,
97
+ trace,
98
+ expected: `valid JSON containing ${path}`,
99
+ actual: "invalid JSON response",
100
+ response,
101
+ message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} did not return valid JSON`,
102
+ })
103
+ );
104
+ return false;
105
+ }
106
+
107
+ const actualValue = getJsonPathValue(jsonResult.value, path);
108
+ return evaluateCheck(
109
+ actualValue,
110
+ title,
111
+ (value) => Boolean(predicate(value)),
112
+ () => ({
113
+ kind: "json-path-assertion",
114
+ key: buildTraceFailureKey(trace, title),
115
+ title,
116
+ message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} JSON path ${path} assertion failed`,
117
+ expected: path,
118
+ actual: toValuePreview(actualValue),
119
+ request: trace ? buildRequestSummary(trace) : null,
120
+ response: trace ? buildResponseSummary(trace) : null,
121
+ })
122
+ );
123
+ }
124
+
125
+ function buildHttpAssertionDetail({ kind, title, trace, expected, actual, response, message }) {
126
+ return {
127
+ kind,
128
+ key: buildTraceFailureKey(trace, title),
129
+ title,
130
+ message,
131
+ expected,
132
+ actual,
133
+ traceId: trace?.id || null,
134
+ request: trace ? buildRequestSummary(trace) : null,
135
+ response: trace ? buildResponseSummary(trace, response) : null,
136
+ };
137
+ }
138
+
139
+ function buildRequestSummary(trace) {
140
+ return {
141
+ id: trace.id,
142
+ requestId: trace.requestId,
143
+ method: trace.method,
144
+ path: trace.path,
145
+ url: trace.url,
146
+ headers: trace.requestHeaders || {},
147
+ };
148
+ }
149
+
150
+ function buildResponseSummary(trace, response = null) {
151
+ return {
152
+ status: (trace.response?.status ?? Number(response?.status ?? 0)) || null,
153
+ contentType: trace.response?.contentType ?? null,
154
+ bodyPreview: trace.response?.bodyPreview ?? toBodyPreview(response),
155
+ bodyTruncated: Boolean(trace.response?.bodyTruncated),
156
+ };
157
+ }
158
+
159
+ function buildTraceFailureKey(trace, title) {
160
+ if (trace?.path && trace?.method) {
161
+ return `${trace.method} ${trace.path} > ${title}`;
162
+ }
163
+ return title;
164
+ }
165
+
166
+ function buildExpectedStatusTitle(expectedValues) {
167
+ return `status is ${formatExpectedValues(expectedValues)}`;
168
+ }
169
+
170
+ function formatExpectedValues(values) {
171
+ if (!Array.isArray(values) || values.length === 0) return "expected";
172
+ if (values.length === 1) return String(values[0]);
173
+ return values.map((value) => String(value)).join(" or ");
174
+ }
175
+
176
+ function normalizeExpectedValues(values) {
177
+ const normalized = (Array.isArray(values) ? values : [values])
178
+ .map((value) => Number(value))
179
+ .filter((value) => Number.isFinite(value));
180
+
181
+ if (normalized.length === 0) {
182
+ throw new Error("expectStatusOneOf requires at least one numeric expected status");
183
+ }
184
+ return normalized;
185
+ }
186
+
187
+ function getJsonPathValue(value, path) {
188
+ const tokens = String(path || "")
189
+ .split(".")
190
+ .map((token) => token.trim())
191
+ .filter(Boolean);
192
+
193
+ let current = value;
194
+ for (const token of tokens) {
195
+ if (current == null || typeof current !== "object") return undefined;
196
+ current = current[token];
197
+ }
198
+ return current;
199
+ }
200
+
201
+ function toValuePreview(value) {
202
+ if (value == null) return value;
203
+ if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
204
+ try {
205
+ return JSON.stringify(value).slice(0, 240);
206
+ } catch {
207
+ return String(value);
208
+ }
209
+ }
210
+
211
+ function normalizeLabel(value, fallback) {
212
+ const normalized = typeof value === "string" ? value.trim() : "";
213
+ return normalized.length > 0 ? normalized : fallback;
214
+ }