@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.
- package/lib/cli/commands/artifacts.mjs +2 -2
- package/lib/cli/commands/logs.mjs +2 -2
- package/lib/cli/commands/show.mjs +2 -2
- package/lib/cli/db.mjs +17 -2
- 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 +41 -7
- package/lib/cli/presentation/run-reporter.test.mjs +80 -0
- package/lib/cli/tui/watch-app.mjs +134 -18
- package/lib/cli/viewer.mjs +146 -4
- package/lib/database/index.mjs +85 -11
- package/lib/database/template-steps.mjs +45 -6
- package/lib/database/template-steps.test.mjs +43 -0
- package/lib/known-failures/index.mjs +1 -1
- package/lib/known-failures/index.test.mjs +46 -0
- package/lib/runner/artifacts.mjs +16 -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 +71 -6
- package/lib/runner/orchestrator.mjs +63 -7
- package/lib/runner/reporting.mjs +52 -2
- package/lib/runner/reporting.test.mjs +80 -2
- package/lib/runner/runtime-contexts.mjs +3 -3
- package/lib/runner/runtime-preparation.mjs +31 -0
- package/lib/runner/setup-operations.mjs +115 -0
- package/lib/runner/setup-operations.test.mjs +94 -0
- package/lib/runner/template-steps.mjs +129 -11
- 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/lib/toolchains/index.mjs +0 -4
- package/package.json +3 -1
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
|
+
}
|
|
@@ -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
|
+
}
|