@elench/testkit 0.1.53 → 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.
- package/lib/cli/presentation/code-frames.mjs +57 -0
- package/lib/cli/presentation/code-frames.test.mjs +71 -0
- package/lib/cli/presentation/colors.mjs +29 -0
- package/lib/cli/presentation/run-reporter.mjs +16 -7
- package/lib/cli/viewer.mjs +109 -4
- package/lib/known-failures/index.mjs +1 -1
- package/lib/known-failures/index.test.mjs +46 -0
- package/lib/runner/default-runtime-errors.mjs +66 -0
- package/lib/runner/default-runtime-runner.mjs +8 -1
- package/lib/runner/failure-details.mjs +31 -0
- package/lib/runner/failure-details.test.mjs +51 -0
- package/lib/runner/formatting.mjs +114 -4
- package/lib/runner/formatting.test.mjs +77 -0
- package/lib/runner/logs.mjs +17 -0
- package/lib/runner/orchestrator.mjs +10 -1
- package/lib/runner/triage.mjs +67 -0
- package/lib/runtime/index.d.ts +60 -0
- package/lib/runtime/index.mjs +12 -0
- package/lib/runtime-src/k6/checks.js +45 -12
- package/lib/runtime-src/k6/http-assertions.js +214 -0
- package/lib/runtime-src/k6/http.js +261 -13
- package/lib/runtime-src/k6/suite.js +46 -1
- package/package.json +3 -1
|
@@ -69,7 +69,7 @@ export function buildCompactRunSummaryLines(
|
|
|
69
69
|
for (const failure of failures) {
|
|
70
70
|
lines.push(` ${failure.file.path}`);
|
|
71
71
|
lines.push(` ${failure.primaryMessage}`);
|
|
72
|
-
for (const detail of failure.
|
|
72
|
+
for (const detail of failure.extraLines.slice(0, 3)) {
|
|
73
73
|
lines.push(` ${detail}`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -209,14 +209,33 @@ function collectFailedFiles(results) {
|
|
|
209
209
|
for (const suite of result.suites || []) {
|
|
210
210
|
for (const file of suite.files || []) {
|
|
211
211
|
if (file.status !== "failed") continue;
|
|
212
|
-
const
|
|
212
|
+
const rankedDetails = rankFailureDetails(file.failureDetails || []);
|
|
213
|
+
const primaryDetail = rankedDetails[0] || null;
|
|
214
|
+
const fallbackMessages = rankedDetails
|
|
213
215
|
.map((detail) => detail.message || detail.title)
|
|
214
216
|
.filter(Boolean)
|
|
215
217
|
.map((message) => sanitizeErrorMessage(String(message).trim()));
|
|
218
|
+
const extraLines = [];
|
|
219
|
+
if (primaryDetail) {
|
|
220
|
+
const responseLine = formatFailureResponsePreview(primaryDetail);
|
|
221
|
+
if (responseLine) extraLines.push(responseLine);
|
|
222
|
+
const triageLine = formatTriageLine(file.triage || null);
|
|
223
|
+
if (triageLine) extraLines.push(triageLine);
|
|
224
|
+
const requestLine = formatFailureRequestHint(primaryDetail);
|
|
225
|
+
if (requestLine) extraLines.push(requestLine);
|
|
226
|
+
} else {
|
|
227
|
+
const triageLine = formatTriageLine(file.triage || null);
|
|
228
|
+
if (triageLine) extraLines.push(triageLine);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const detail of rankedDetails.slice(primaryDetail ? 1 : 0)) {
|
|
232
|
+
const line = sanitizeErrorMessage(String(detail.message || detail.title || "").trim());
|
|
233
|
+
if (line && !extraLines.includes(line)) extraLines.push(line);
|
|
234
|
+
}
|
|
216
235
|
failures.push({
|
|
217
236
|
file,
|
|
218
|
-
primaryMessage:
|
|
219
|
-
|
|
237
|
+
primaryMessage: resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages),
|
|
238
|
+
extraLines,
|
|
220
239
|
});
|
|
221
240
|
}
|
|
222
241
|
}
|
|
@@ -224,6 +243,97 @@ function collectFailedFiles(results) {
|
|
|
224
243
|
return failures.sort((left, right) => left.file.path.localeCompare(right.file.path));
|
|
225
244
|
}
|
|
226
245
|
|
|
246
|
+
function resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages) {
|
|
247
|
+
if (primaryDetail?.message) {
|
|
248
|
+
return sanitizeErrorMessage(String(primaryDetail.message).trim());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fileError = sanitizeErrorMessage(file.error || "");
|
|
252
|
+
if (fileError && !isThresholdWrapperMessage(fileError)) {
|
|
253
|
+
return fileError;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (fallbackMessages.length > 0) {
|
|
257
|
+
return fallbackMessages[0];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (fileError) {
|
|
261
|
+
return fileError;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return sanitizeErrorMessage(suite.error || "Failed");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function rankFailureDetails(details) {
|
|
268
|
+
return [...(Array.isArray(details) ? details : [])].sort((left, right) => {
|
|
269
|
+
return failureDetailRank(left) - failureDetailRank(right) || String(left?.key || "").localeCompare(String(right?.key || ""));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function failureDetailRank(detail) {
|
|
274
|
+
if (detail?.kind === "http-assertion") return 1;
|
|
275
|
+
if (detail?.request && detail?.response) return 2;
|
|
276
|
+
if (detail?.location || detail?.stack) return 3;
|
|
277
|
+
if (detail?.message) return 4;
|
|
278
|
+
return 5;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatFailureResponsePreview(detail) {
|
|
282
|
+
const bodyPreview = detail?.response?.bodyPreview;
|
|
283
|
+
if (!bodyPreview) return null;
|
|
284
|
+
return `response: ${sanitizeInline(bodyPreview, 220)}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatFailureRequestHint(detail) {
|
|
288
|
+
const method = detail?.request?.method;
|
|
289
|
+
const path = detail?.request?.path;
|
|
290
|
+
if (!method || !path) return null;
|
|
291
|
+
const requestId = detail?.request?.requestId;
|
|
292
|
+
if (requestId) {
|
|
293
|
+
return `logs: requestId=${requestId}`;
|
|
294
|
+
}
|
|
295
|
+
return `request: ${method} ${path}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatTriageLine(triage) {
|
|
299
|
+
if (!triage) return null;
|
|
300
|
+
if (triage.status === "untriaged") return "triage: untriaged";
|
|
301
|
+
|
|
302
|
+
const entry = triage.entries?.[0];
|
|
303
|
+
if (!entry?.issue) {
|
|
304
|
+
if (triage.status === "validation_unavailable") {
|
|
305
|
+
const reason = triage.availability?.reason ? ` (${triage.availability.reason})` : "";
|
|
306
|
+
return `triage: validation unavailable${reason}`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const issueLabel = `#${entry.issue.number}`;
|
|
312
|
+
const validationStatus = entry.validationStatus || null;
|
|
313
|
+
if (validationStatus === "closed_but_failing") {
|
|
314
|
+
return `triage: known issue ${issueLabel} closed but still failing`;
|
|
315
|
+
}
|
|
316
|
+
if (validationStatus === "validation_unavailable") {
|
|
317
|
+
const reason = triage.availability?.mode === "cache" ? "cache" : "validation unavailable";
|
|
318
|
+
return `triage: known issue ${issueLabel} (${reason})`;
|
|
319
|
+
}
|
|
320
|
+
if (entry.github?.state === "open" || entry.state === "open") {
|
|
321
|
+
return `triage: known issue ${issueLabel} open`;
|
|
322
|
+
}
|
|
323
|
+
if (entry.github?.state === "closed" || entry.state === "closed") {
|
|
324
|
+
return `triage: known issue ${issueLabel} closed`;
|
|
325
|
+
}
|
|
326
|
+
return `triage: known issue ${issueLabel}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isThresholdWrapperMessage(message) {
|
|
330
|
+
return /Default runtime thresholds failed:/.test(String(message || ""));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function sanitizeInline(message, maxLength = 180) {
|
|
334
|
+
return String(message || "").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
335
|
+
}
|
|
336
|
+
|
|
227
337
|
function collectServiceErrors(results) {
|
|
228
338
|
const items = [];
|
|
229
339
|
for (const result of results) {
|
|
@@ -162,4 +162,81 @@ describe("runner formatting", () => {
|
|
|
162
162
|
expect(lines.join("\n")).toContain("2 closed issues still failing");
|
|
163
163
|
expect(lines.join("\n")).toContain("1 title mismatch");
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
it("prefers structured HTTP assertion details over threshold wrapper text", () => {
|
|
167
|
+
const lines = buildRunSummaryLines(
|
|
168
|
+
[
|
|
169
|
+
{
|
|
170
|
+
name: "api",
|
|
171
|
+
skipped: false,
|
|
172
|
+
failed: true,
|
|
173
|
+
suiteCount: 1,
|
|
174
|
+
completedSuiteCount: 1,
|
|
175
|
+
skippedSuiteCount: 0,
|
|
176
|
+
failedSuiteCount: 1,
|
|
177
|
+
totalFileCount: 1,
|
|
178
|
+
passedFileCount: 0,
|
|
179
|
+
failedFileCount: 1,
|
|
180
|
+
skippedFileCount: 0,
|
|
181
|
+
notRunFileCount: 0,
|
|
182
|
+
durationMs: 500,
|
|
183
|
+
suites: [
|
|
184
|
+
{
|
|
185
|
+
failed: true,
|
|
186
|
+
type: "int",
|
|
187
|
+
name: "default",
|
|
188
|
+
framework: "k6",
|
|
189
|
+
failedFiles: ["__testkit__/health/health.int.testkit.ts"],
|
|
190
|
+
durationMs: 500,
|
|
191
|
+
files: [
|
|
192
|
+
{
|
|
193
|
+
path: "__testkit__/health/health.int.testkit.ts",
|
|
194
|
+
status: "failed",
|
|
195
|
+
error: "Default runtime thresholds failed: checks(rate==1.0)",
|
|
196
|
+
failureDetails: [
|
|
197
|
+
{
|
|
198
|
+
kind: "http-assertion",
|
|
199
|
+
key: "GET /health > status is 200",
|
|
200
|
+
title: "status is 200",
|
|
201
|
+
message: "GET /health expected 200, got 404",
|
|
202
|
+
request: {
|
|
203
|
+
method: "GET",
|
|
204
|
+
path: "/health",
|
|
205
|
+
requestId: "req-1",
|
|
206
|
+
},
|
|
207
|
+
response: {
|
|
208
|
+
status: 404,
|
|
209
|
+
bodyPreview: '{"error":"nope"}',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
triage: {
|
|
214
|
+
status: "known_failure",
|
|
215
|
+
entries: [
|
|
216
|
+
{
|
|
217
|
+
id: "health-is-bad",
|
|
218
|
+
state: "open",
|
|
219
|
+
issue: {
|
|
220
|
+
repo: "acme/example",
|
|
221
|
+
number: 42,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
error: null,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
errors: [],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
500
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(lines.join("\n")).toContain("GET /health expected 200, got 404");
|
|
238
|
+
expect(lines.join("\n")).toContain('response: {"error":"nope"}');
|
|
239
|
+
expect(lines.join("\n")).toContain("triage: known issue #42 open");
|
|
240
|
+
expect(lines.join("\n")).not.toContain("Default runtime thresholds failed: checks(rate==1.0)\n response:");
|
|
241
|
+
});
|
|
165
242
|
});
|
package/lib/runner/logs.mjs
CHANGED
|
@@ -59,6 +59,23 @@ export function readLogTail(absolutePath, lineCount = 80) {
|
|
|
59
59
|
return lines.slice(Math.max(0, lines.length - lineCount));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export function findLogSliceByRequestId(absolutePath, requestId, contextLines = 2) {
|
|
63
|
+
if (!absolutePath || !requestId || !fs.existsSync(absolutePath)) return [];
|
|
64
|
+
const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
65
|
+
const matches = [];
|
|
66
|
+
|
|
67
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
68
|
+
if (!lines[index].includes(requestId)) continue;
|
|
69
|
+
const start = Math.max(0, index - contextLines);
|
|
70
|
+
const end = Math.min(lines.length, index + contextLines + 1);
|
|
71
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
72
|
+
if (!matches.includes(lines[cursor])) matches.push(lines[cursor]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return matches;
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
function sanitizePathSegment(value) {
|
|
63
80
|
return String(value)
|
|
64
81
|
.trim()
|
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
summarizeDbBackend,
|
|
16
16
|
} from "./results.mjs";
|
|
17
17
|
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
applyKnownFailureIssueValidationToArtifacts,
|
|
20
|
+
applyKnownFailuresToArtifacts,
|
|
21
|
+
loadKnownFailuresConfig,
|
|
22
|
+
} from "./triage.mjs";
|
|
19
23
|
import { formatError } from "./formatting.mjs";
|
|
20
24
|
import {
|
|
21
25
|
shouldFailKnownFailureIssueValidation,
|
|
@@ -230,6 +234,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
230
234
|
config: configs[0]?.testkit?.reporting?.issueValidation || null,
|
|
231
235
|
gitMetadata: metadata.git,
|
|
232
236
|
});
|
|
237
|
+
applyKnownFailureIssueValidationToArtifacts(
|
|
238
|
+
enrichedArtifacts.runArtifact,
|
|
239
|
+
enrichedArtifacts.statusArtifact,
|
|
240
|
+
knownFailureIssueValidation
|
|
241
|
+
);
|
|
233
242
|
attachKnownFailureIssueValidation(
|
|
234
243
|
enrichedArtifacts.runArtifact,
|
|
235
244
|
enrichedArtifacts.statusArtifact,
|
package/lib/runner/triage.mjs
CHANGED
|
@@ -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
|
+
}
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|