@molecule/api-ai-tools 1.9.10 → 1.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -5
- package/dist/trace-artifacts.d.ts +43 -5
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +177 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-18T07:36:48.788Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -404,6 +404,28 @@ interface TraceRequest {
|
|
|
404
404
|
* fixture), in which case only recording order is available.
|
|
405
405
|
*/
|
|
406
406
|
startedAt?: number
|
|
407
|
+
/**
|
|
408
|
+
* Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
|
|
409
|
+
*
|
|
410
|
+
* A 401 cannot be read without this: the API's own body said
|
|
411
|
+
* `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
|
|
412
|
+
* when the header's absence proved the CLIENT never attached the token
|
|
413
|
+
* (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
|
|
414
|
+
* refused a token it was sent.
|
|
415
|
+
*/
|
|
416
|
+
sentAuthorization?: boolean
|
|
417
|
+
/**
|
|
418
|
+
* The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
|
|
419
|
+
* no header or its first word does not parse as a scheme (a value pasted without one is a
|
|
420
|
+
* credential, and is never copied here).
|
|
421
|
+
*/
|
|
422
|
+
authorization?: string
|
|
423
|
+
/**
|
|
424
|
+
* Whether the request carried a `Cookie` header — never the cookies themselves. An app that
|
|
425
|
+
* signs in with a session cookie has no `Authorization` header by design, so this tells a
|
|
426
|
+
* credential-less request from one whose credential is a cookie.
|
|
427
|
+
*/
|
|
428
|
+
sentCookie?: boolean
|
|
407
429
|
}
|
|
408
430
|
```
|
|
409
431
|
|
|
@@ -455,7 +477,7 @@ interface TraceAction {
|
|
|
455
477
|
|
|
456
478
|
#### `TraceArtifact`
|
|
457
479
|
|
|
458
|
-
What one trace says: the test it belongs to, the requests it made, and its console
|
|
480
|
+
What one trace says: the test it belongs to, the requests it made, and its console lines.
|
|
459
481
|
|
|
460
482
|
```typescript
|
|
461
483
|
interface TraceArtifact {
|
|
@@ -477,7 +499,11 @@ interface TraceArtifact {
|
|
|
477
499
|
* Absent when the trace recorded neither, and on traces read before this field existed.
|
|
478
500
|
*/
|
|
479
501
|
lead?: TraceRequest
|
|
480
|
-
/**
|
|
502
|
+
/**
|
|
503
|
+
* The console lines and page errors the page raised: its errors first, then a line that names a
|
|
504
|
+
* session decision or comes from a molecule package, then any other warning, then the browser's
|
|
505
|
+
* own resource error ({@link traceArtifactsScript}).
|
|
506
|
+
*/
|
|
481
507
|
console: string[]
|
|
482
508
|
/**
|
|
483
509
|
* The test's LAST user action before the failing assertion and the requests it caused — what the
|
|
@@ -1819,7 +1845,7 @@ function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool
|
|
|
1819
1845
|
|
|
1820
1846
|
The Node program the sandbox runs to read a failed run's traces: `node <script> <dir>` prints one
|
|
1821
1847
|
`TRACE <json>` line per `trace.zip` under `<dir>` (newest first, {@link MAX_TRACE_ARTIFACTS} of
|
|
1822
|
-
them), each with the test it belongs to, the requests its page made and its console
|
|
1848
|
+
them), each with the test it belongs to, the requests its page made and its console lines.
|
|
1823
1849
|
|
|
1824
1850
|
It is a standalone program, not an import, for the reason `@molecule/api-code-sandbox` runs it:
|
|
1825
1851
|
Playwright's own zip reader is not reachable from a project's `node_modules`, and a dependency
|
|
@@ -3221,7 +3247,8 @@ const MAX_TRACED_REQUESTS: 6
|
|
|
3221
3247
|
|
|
3222
3248
|
#### `MAX_TRACED_CONSOLE`
|
|
3223
3249
|
|
|
3224
|
-
How many console
|
|
3250
|
+
How many console lines one test's trace contributes: errors ahead of a session or molecule-package
|
|
3251
|
+
line, both ahead of a plain warning and of the browser's own resource error.
|
|
3225
3252
|
|
|
3226
3253
|
```typescript
|
|
3227
3254
|
const MAX_TRACED_CONSOLE: 4
|
|
@@ -67,6 +67,28 @@ export interface TraceRequest {
|
|
|
67
67
|
* fixture), in which case only recording order is available.
|
|
68
68
|
*/
|
|
69
69
|
startedAt?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
|
|
72
|
+
*
|
|
73
|
+
* A 401 cannot be read without this: the API's own body said
|
|
74
|
+
* `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
|
|
75
|
+
* when the header's absence proved the CLIENT never attached the token
|
|
76
|
+
* (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
|
|
77
|
+
* refused a token it was sent.
|
|
78
|
+
*/
|
|
79
|
+
sentAuthorization?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
|
|
82
|
+
* no header or its first word does not parse as a scheme (a value pasted without one is a
|
|
83
|
+
* credential, and is never copied here).
|
|
84
|
+
*/
|
|
85
|
+
authorization?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the request carried a `Cookie` header — never the cookies themselves. An app that
|
|
88
|
+
* signs in with a session cookie has no `Authorization` header by design, so this tells a
|
|
89
|
+
* credential-less request from one whose credential is a cookie.
|
|
90
|
+
*/
|
|
91
|
+
sentCookie?: boolean;
|
|
70
92
|
}
|
|
71
93
|
/**
|
|
72
94
|
* The test's LAST user action before the failing assertion, and the requests it caused.
|
|
@@ -104,7 +126,7 @@ export interface TraceAction {
|
|
|
104
126
|
/** When it finished, or null when the run ended before it did. */
|
|
105
127
|
end: number | null;
|
|
106
128
|
}
|
|
107
|
-
/** What one trace says: the test it belongs to, the requests it made, and its console
|
|
129
|
+
/** What one trace says: the test it belongs to, the requests it made, and its console lines. */
|
|
108
130
|
export interface TraceArtifact {
|
|
109
131
|
/** The failure's spec file, relative to the run's directory, when the trace names one. */
|
|
110
132
|
spec: string | null;
|
|
@@ -124,7 +146,11 @@ export interface TraceArtifact {
|
|
|
124
146
|
* Absent when the trace recorded neither, and on traces read before this field existed.
|
|
125
147
|
*/
|
|
126
148
|
lead?: TraceRequest;
|
|
127
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* The console lines and page errors the page raised: its errors first, then a line that names a
|
|
151
|
+
* session decision or comes from a molecule package, then any other warning, then the browser's
|
|
152
|
+
* own resource error ({@link traceArtifactsScript}).
|
|
153
|
+
*/
|
|
128
154
|
console: string[];
|
|
129
155
|
/**
|
|
130
156
|
* The test's LAST user action before the failing assertion and the requests it caused — what the
|
|
@@ -145,7 +171,10 @@ export interface TraceArtifact {
|
|
|
145
171
|
export declare const MAX_TRACE_ARTIFACTS = 12;
|
|
146
172
|
/** How many requests one test's trace contributes. */
|
|
147
173
|
export declare const MAX_TRACED_REQUESTS = 6;
|
|
148
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* How many console lines one test's trace contributes: errors ahead of a session or molecule-package
|
|
176
|
+
* line, both ahead of a plain warning and of the browser's own resource error.
|
|
177
|
+
*/
|
|
149
178
|
export declare const MAX_TRACED_CONSOLE = 4;
|
|
150
179
|
/** How many of the page's own actions one test's trace contributes, the LAST ones. */
|
|
151
180
|
export declare const MAX_TRACED_ACTIONS = 40;
|
|
@@ -170,7 +199,7 @@ export declare const TRACE_NOTE_LEAD_GOT_CHARS = 200;
|
|
|
170
199
|
/**
|
|
171
200
|
* The Node program the sandbox runs to read a failed run's traces: `node <script> <dir>` prints one
|
|
172
201
|
* `TRACE <json>` line per `trace.zip` under `<dir>` (newest first, {@link MAX_TRACE_ARTIFACTS} of
|
|
173
|
-
* them), each with the test it belongs to, the requests its page made and its console
|
|
202
|
+
* them), each with the test it belongs to, the requests its page made and its console lines.
|
|
174
203
|
*
|
|
175
204
|
* It is a standalone program, not an import, for the reason `@molecule/api-code-sandbox` runs it:
|
|
176
205
|
* Playwright's own zip reader is not reachable from a project's `node_modules`, and a dependency
|
|
@@ -282,7 +311,7 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
282
311
|
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
283
312
|
*
|
|
284
313
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
285
|
-
* needs a recorded response, a real failure or a console
|
|
314
|
+
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
286
315
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
287
316
|
* earlier walk → the rest ({@link traceRowTier}). Without that the note spent all five rows on four
|
|
288
317
|
* traces of one `afterEach` line whose every request was `-1`, and left the favorite cause — the
|
|
@@ -306,6 +335,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
306
335
|
* request that never reached the API. The prefix is READ (the caller passes what the app's client
|
|
307
336
|
* config names; the traces add what their own requests show) and never assumed to be `/api`.
|
|
308
337
|
*
|
|
338
|
+
* A 401/403 on the app's own origin is answered from the request's own credential record instead
|
|
339
|
+
* ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
|
|
340
|
+
* token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
|
|
341
|
+
* header the API then refused moves the answer to the server. The handler sentence is kept for
|
|
342
|
+
* the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
|
|
343
|
+
* trace that proves the header was never sent cannot be explained by the handler.
|
|
344
|
+
* xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
|
|
345
|
+
* `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
|
|
346
|
+
*
|
|
309
347
|
* @param items - The traces {@link parseTraceArtifacts} read.
|
|
310
348
|
* @param target - Where the run went — the deployed host, as the note names it.
|
|
311
349
|
* @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
|
package/dist/trace-artifacts.js
CHANGED
|
@@ -33,7 +33,10 @@ import { isPlaywrightRunCommand, redactSecrets } from './utilities.js';
|
|
|
33
33
|
export const MAX_TRACE_ARTIFACTS = 12;
|
|
34
34
|
/** How many requests one test's trace contributes. */
|
|
35
35
|
export const MAX_TRACED_REQUESTS = 6;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* How many console lines one test's trace contributes: errors ahead of a session or molecule-package
|
|
38
|
+
* line, both ahead of a plain warning and of the browser's own resource error.
|
|
39
|
+
*/
|
|
37
40
|
export const MAX_TRACED_CONSOLE = 4;
|
|
38
41
|
/** How many of the page's own actions one test's trace contributes, the LAST ones. */
|
|
39
42
|
export const MAX_TRACED_ACTIONS = 40;
|
|
@@ -60,7 +63,7 @@ const TRACE_NOTE_FAILURE_CHARS = 120;
|
|
|
60
63
|
/**
|
|
61
64
|
* The Node program the sandbox runs to read a failed run's traces: `node <script> <dir>` prints one
|
|
62
65
|
* `TRACE <json>` line per `trace.zip` under `<dir>` (newest first, {@link MAX_TRACE_ARTIFACTS} of
|
|
63
|
-
* them), each with the test it belongs to, the requests its page made and its console
|
|
66
|
+
* them), each with the test it belongs to, the requests its page made and its console lines.
|
|
64
67
|
*
|
|
65
68
|
* It is a standalone program, not an import, for the reason `@molecule/api-code-sandbox` runs it:
|
|
66
69
|
* Playwright's own zip reader is not reachable from a project's `node_modules`, and a dependency
|
|
@@ -95,6 +98,20 @@ const MAX_CONSOLE = ${MAX_TRACED_CONSOLE}
|
|
|
95
98
|
const MAX_ACTIONS = ${MAX_TRACED_ACTIONS}
|
|
96
99
|
const MAX_BODY = ${MAX_TRACED_BODY_CHARS}
|
|
97
100
|
const MAX_URL = 200
|
|
101
|
+
// A console line that is not an error but still has to reach the note (m148): one a molecule
|
|
102
|
+
// package wrote — it names the package, the \`[molecule/…]\` prefix, or the \`[scope]\` prefix
|
|
103
|
+
// molecule's own logger writes for a named module ([auth], [http], [i18n]) — or one that names a
|
|
104
|
+
// session decision in the page's own words. \`@molecule/app-auth\` logged "Token refresh failed,
|
|
105
|
+
// logging out" at WARNING level, and this reader kept error entries only, so every trace note
|
|
106
|
+
// carried the browser's generic "Failed to load resource: 401" while the platform's own package was
|
|
107
|
+
// saying, in words, that it had signed the user out.
|
|
108
|
+
const MOLECULE_CONSOLE = /@molecule\\/|\\[molecule[:\\/][^\\]]*\\]|^\\s*\\[[a-z][a-z0-9-]*\\]/
|
|
109
|
+
const SESSION_CONSOLE = /\\b(?:auth(?:entication|enticated|orization|orized)?|session|token|sign(?:ed|ing)?[\\s-]?out|log(?:ged|ging)?[\\s-]?out|refresh(?:ed|ing)?\\s+failed|no\\s+provider)\\b/i
|
|
110
|
+
|
|
111
|
+
// Whether a console line belongs in the note even though it is not at error level.
|
|
112
|
+
function consoleSignal(text) {
|
|
113
|
+
return MOLECULE_CONSOLE.test(text) || SESSION_CONSOLE.test(text)
|
|
114
|
+
}
|
|
98
115
|
|
|
99
116
|
function walk(dir, depth, out) {
|
|
100
117
|
if (depth < 0) return
|
|
@@ -226,12 +243,34 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
226
243
|
const response = snapshot.response || {}
|
|
227
244
|
const type = String(snapshot._resourceType || '')
|
|
228
245
|
if (!origin) origin = type === 'document' ? originOf(String(request.url)) : ''
|
|
246
|
+
// What the request carried for CREDENTIALS — never the secret itself. A 401 on the app's own
|
|
247
|
+
// API cannot be read without this: m148's favorite POST answered 401 with no Authorization
|
|
248
|
+
// header at all, so the client never attached the token, and the note sent the run to the
|
|
249
|
+
// API handler instead (xr-molecule-flash-m148 +29:18). Presence is recorded as a boolean and
|
|
250
|
+
// only the scheme WORD (\`Bearer\`, \`Token\`) is kept; the value is a live credential and never
|
|
251
|
+
// leaves the trace. A first word that does not parse as a scheme (\`Authorization: <raw jwt>\`)
|
|
252
|
+
// is recorded as presence only, so a pasted token can never ride along as a "scheme".
|
|
253
|
+
const headerList = Array.isArray(request.headers) ? request.headers : []
|
|
254
|
+
const authHeader = headerList.find(function (h) {
|
|
255
|
+
return h && typeof h.name === 'string' && /^authorization$/i.test(h.name)
|
|
256
|
+
})
|
|
257
|
+
const authValue =
|
|
258
|
+
authHeader && typeof authHeader.value === 'string' ? String(authHeader.value).trim() : ''
|
|
259
|
+
const authSpace = authValue.search(/\\s/)
|
|
260
|
+
const authWord = authSpace > 0 ? authValue.slice(0, authSpace) : ''
|
|
261
|
+
const authScheme = /^[A-Za-z][A-Za-z0-9._~+/-]{0,23}$/.test(authWord) ? authWord : undefined
|
|
262
|
+
const sentCookie = headerList.some(function (h) {
|
|
263
|
+
return h && typeof h.name === 'string' && /^cookie$/i.test(h.name)
|
|
264
|
+
})
|
|
229
265
|
requests.push({
|
|
230
266
|
method: String(request.method).toUpperCase(),
|
|
231
267
|
path: String(request.url).slice(0, MAX_URL),
|
|
232
268
|
status: Number(response.status) || 0,
|
|
233
269
|
failure: typeof response._failureText === 'string' ? response._failureText : undefined,
|
|
234
270
|
type: type,
|
|
271
|
+
sentAuthorization: authHeader !== undefined,
|
|
272
|
+
authorization: authScheme,
|
|
273
|
+
sentCookie: sentCookie,
|
|
235
274
|
// The HAR entry carries the monotonic clock the trace's action entries use, so a request
|
|
236
275
|
// can be attributed to the action that caused it (see \`lastAction\` below).
|
|
237
276
|
startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
|
|
@@ -413,6 +452,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
413
452
|
...(r.sent ? { sent: r.sent } : {}),
|
|
414
453
|
...(r.got ? { got: r.got } : {}),
|
|
415
454
|
type: r.type || undefined,
|
|
455
|
+
...(r.sentAuthorization ? { sentAuthorization: true } : {}),
|
|
456
|
+
...(r.authorization ? { authorization: r.authorization } : {}),
|
|
457
|
+
...(r.sentCookie ? { sentCookie: true } : {}),
|
|
416
458
|
}
|
|
417
459
|
}
|
|
418
460
|
// The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
|
|
@@ -508,13 +550,18 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
508
550
|
chosen.push(wire(r))
|
|
509
551
|
}
|
|
510
552
|
const consoleErrors = []
|
|
553
|
+
const consoleSignals = []
|
|
554
|
+
const consoleWarnings = []
|
|
511
555
|
let spec = null
|
|
512
556
|
let line = 0
|
|
513
557
|
for (const entry of trace) {
|
|
514
558
|
if (!entry) continue
|
|
515
|
-
if (entry.type === 'console' &&
|
|
516
|
-
consoleErrors.push(entry.text)
|
|
517
|
-
|
|
559
|
+
if (entry.type === 'console' && typeof entry.text === 'string') {
|
|
560
|
+
if (entry.messageType === 'error') consoleErrors.push(entry.text)
|
|
561
|
+
else if (consoleSignal(entry.text)) consoleSignals.push(entry.text)
|
|
562
|
+
else if (entry.messageType === 'warning' || entry.messageType === 'warn')
|
|
563
|
+
consoleWarnings.push(entry.text)
|
|
564
|
+
} else if (entry.type === 'event' && entry.method === 'pageError') {
|
|
518
565
|
const message = messageOf(entry.params)
|
|
519
566
|
if (message) consoleErrors.push(message)
|
|
520
567
|
} else if (entry.type === 'before' && Array.isArray(entry.stack) && entry.stack.length) {
|
|
@@ -526,9 +573,13 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
526
573
|
}
|
|
527
574
|
}
|
|
528
575
|
// Chromium reports every failed request as a console error too; where the page logged something
|
|
529
|
-
// of its own about it, that is the line worth reading.
|
|
576
|
+
// of its own about it, that is the line worth reading. Errors lead, then the lines that name a
|
|
577
|
+
// session decision or come from a molecule package (m148), then any other warning — the browser's
|
|
578
|
+
// resource error is last, because it is evidence of a failed request, not of a cause, and the
|
|
579
|
+
// platform's own package saying "logging out" must not be ranked below it.
|
|
530
580
|
const specific = consoleErrors.filter(function (text) { return !/^Failed to load resource/.test(text) })
|
|
531
|
-
const keptConsole = specific.
|
|
581
|
+
const keptConsole = specific.concat(consoleSignals, consoleWarnings)
|
|
582
|
+
if (specific.length === 0) keptConsole.push.apply(keptConsole, consoleErrors)
|
|
532
583
|
if (chosen.length === 0 && keptConsole.length === 0 && actions.length === 0) continue
|
|
533
584
|
process.stdout.write('TRACE ' + JSON.stringify({
|
|
534
585
|
dir: path.relative(ROOT, path.dirname(one.file)),
|
|
@@ -564,6 +615,12 @@ function parseTraceRequest(value) {
|
|
|
564
615
|
...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
|
|
565
616
|
...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
|
|
566
617
|
...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
|
|
618
|
+
// The scheme word only: a raw header value is a live credential and is never parsed in.
|
|
619
|
+
...(r.sentAuthorization === true ? { sentAuthorization: true } : {}),
|
|
620
|
+
...(typeof r.authorization === 'string' && r.authorization
|
|
621
|
+
? { authorization: r.authorization }
|
|
622
|
+
: {}),
|
|
623
|
+
...(r.sentCookie === true ? { sentCookie: true } : {}),
|
|
567
624
|
};
|
|
568
625
|
}
|
|
569
626
|
/**
|
|
@@ -910,7 +967,7 @@ function repeatsEarlierFailure(item, earlier) {
|
|
|
910
967
|
}
|
|
911
968
|
/**
|
|
912
969
|
* Whether a trace holds evidence the note can explain: a request that got a RECORDED response, a
|
|
913
|
-
* real failure (4xx/5xx or a network error), or a console
|
|
970
|
+
* real failure (4xx/5xx or a network error), or a console line. A request with no response is
|
|
914
971
|
* not evidence (m112 +53:46): four of the note's five rows were traces of one `afterEach` line
|
|
915
972
|
* whose every request was `-1`, so the favorite cause — repeated in all three walks — stayed
|
|
916
973
|
* hidden in "… and 7 more traced failures".
|
|
@@ -921,7 +978,7 @@ function repeatsEarlierFailure(item, earlier) {
|
|
|
921
978
|
* `page.goto` timed out ({@link stalledDocumentSubresource}).
|
|
922
979
|
*
|
|
923
980
|
* @param item - One traced failure.
|
|
924
|
-
* @returns True when the trace holds a request or console
|
|
981
|
+
* @returns True when the trace holds a request or console line worth a row.
|
|
925
982
|
*/
|
|
926
983
|
function traceRowEvidence(item) {
|
|
927
984
|
return (item.console.length > 0 ||
|
|
@@ -1005,6 +1062,94 @@ function sameOriginPath(request) {
|
|
|
1005
1062
|
return null;
|
|
1006
1063
|
return raw.replace(/\/+$/, '');
|
|
1007
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* The sentence a 401/403 on the app's own API gets: which half of the wire refused the request.
|
|
1067
|
+
*
|
|
1068
|
+
* A fixed "read the handler this route reaches" line is the wrong half for a credential refusal.
|
|
1069
|
+
* The request's own `Authorization` header — recorded by the trace reader as its scheme WORD, never
|
|
1070
|
+
* its value — decides: absent means the CLIENT never attached the token (a session store that the
|
|
1071
|
+
* HTTP client does not read, a bare `fetch`, an auth bond the app replaced), while present means
|
|
1072
|
+
* the SERVER refused a token it was sent (scheme or issuer). A 403 is a permission answer, not a
|
|
1073
|
+
* credential one, and says so.
|
|
1074
|
+
*
|
|
1075
|
+
* xr-molecule-flash-m148 (+29:18): `POST /api/articles/…/favorite → 401 {"errors":{"token":["is
|
|
1076
|
+
* missing"]}}` was answered with the handler sentence for six local walks, while the trace proved
|
|
1077
|
+
* the request carried no `Authorization` header at all and the app's hand-rolled session store
|
|
1078
|
+
* never handed its token to the shared client.
|
|
1079
|
+
*
|
|
1080
|
+
* The trace cannot name the file that made the call: a real HAR entry carries the request, response,
|
|
1081
|
+
* `_resourceType`, `_monotonicTime` and a frame ref, but no initiator stack. So the sentence names
|
|
1082
|
+
* the shared client and the auth bond instead of guessing a file.
|
|
1083
|
+
*
|
|
1084
|
+
* @param request - The refused request, a 401 or 403 on the app's own origin.
|
|
1085
|
+
* @returns The sentence the note appends.
|
|
1086
|
+
*/
|
|
1087
|
+
function authRefusalSentence(request) {
|
|
1088
|
+
const label = requestLabel(request);
|
|
1089
|
+
// The header's scheme, when it parsed; presence alone is still the server's half of the wire.
|
|
1090
|
+
const carried = request.authorization !== undefined
|
|
1091
|
+
? `with \`Authorization: ${request.authorization} …\``
|
|
1092
|
+
: 'with an `Authorization` header';
|
|
1093
|
+
if (request.status === 403) {
|
|
1094
|
+
return request.sentAuthorization === true
|
|
1095
|
+
? `The browser sent \`${label}\` ${carried} and the API answered 403 — the token was ` +
|
|
1096
|
+
'accepted but this user may not do it: read the route’s role/permission check in your ' +
|
|
1097
|
+
'own source, and the account the session belongs to.'
|
|
1098
|
+
: `The browser sent \`${label}\` and the API answered 403 — the request carried no ` +
|
|
1099
|
+
'`Authorization` header, so either the client did not attach the token (check that the ' +
|
|
1100
|
+
'request goes through the app’s HTTP client that the auth bond configures, and that the ' +
|
|
1101
|
+
'token is stored where the client reads it) or the route refuses this user: read the ' +
|
|
1102
|
+
'route’s role/permission check in your own source.';
|
|
1103
|
+
}
|
|
1104
|
+
if (request.sentAuthorization === true) {
|
|
1105
|
+
return (`The browser sent \`${label}\` ${carried} and the API refused it: the token was sent and ` +
|
|
1106
|
+
'rejected — the scheme (the client’s `authScheme`, e.g. `Token` vs `Bearer`) or the ' +
|
|
1107
|
+
'token’s issuer differs from what the server verifies; read the verify middleware this ' +
|
|
1108
|
+
'route reaches.');
|
|
1109
|
+
}
|
|
1110
|
+
return (`The browser sent \`${label}\` with NO \`Authorization\` header — the client did not attach ` +
|
|
1111
|
+
'the token: check that the request goes through the app’s HTTP client that the auth bond ' +
|
|
1112
|
+
'configures (not a bare `fetch`), and that the token is stored where the client reads it. ' +
|
|
1113
|
+
'The shared client carries it through `setAuthToken` — the auth bond ' +
|
|
1114
|
+
'(`createDefaultAuthClientWithHttpSync` in `@molecule/app-bonds-default-react`) mirrors the ' +
|
|
1115
|
+
'auth client’s token there, and a session store of your own has to call it too. ' +
|
|
1116
|
+
(request.sentCookie
|
|
1117
|
+
? 'The request did carry a `Cookie` header, so if this app signs in with a session cookie ' +
|
|
1118
|
+
'that is the credential the server did not accept. '
|
|
1119
|
+
: '') +
|
|
1120
|
+
'The API handler is not the problem.');
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* The note's credential-refusal advice, one sentence per distinct shape — at most two.
|
|
1124
|
+
*
|
|
1125
|
+
* The shapes are `(status, Authorization scheme, cookie present)`: m148's ten walks repeated one
|
|
1126
|
+
* shape (`401`, no header) on every failing test, and repeating its sentence per row would bury
|
|
1127
|
+
* the fact rather than state it.
|
|
1128
|
+
*
|
|
1129
|
+
* @param items - The traces the note is about.
|
|
1130
|
+
* @returns The sentences to append (each led by a space), or '' when no same-origin 401/403 was
|
|
1131
|
+
* traced.
|
|
1132
|
+
*/
|
|
1133
|
+
function authRefusalAdvice(items) {
|
|
1134
|
+
const sentences = [];
|
|
1135
|
+
const seen = new Set();
|
|
1136
|
+
for (const item of items) {
|
|
1137
|
+
for (const request of item.requests) {
|
|
1138
|
+
if (request.status !== 401 && request.status !== 403)
|
|
1139
|
+
continue;
|
|
1140
|
+
if (sameOriginPath(request) === null)
|
|
1141
|
+
continue;
|
|
1142
|
+
const key = `${request.status} ${request.sentAuthorization ? (request.authorization ?? 'header') : 'none'} ${request.sentCookie ? 'cookie' : ''}`;
|
|
1143
|
+
if (seen.has(key))
|
|
1144
|
+
continue;
|
|
1145
|
+
seen.add(key);
|
|
1146
|
+
sentences.push(authRefusalSentence(request));
|
|
1147
|
+
if (sentences.length === 2)
|
|
1148
|
+
return ` ${sentences.join(' ')}`;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return sentences.length === 0 ? '' : ` ${sentences.join(' ')}`;
|
|
1152
|
+
}
|
|
1008
1153
|
/**
|
|
1009
1154
|
* The API path prefixes the traced requests themselves show.
|
|
1010
1155
|
*
|
|
@@ -1112,7 +1257,7 @@ function missingApiPrefixSentence(request) {
|
|
|
1112
1257
|
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
1113
1258
|
*
|
|
1114
1259
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
1115
|
-
* needs a recorded response, a real failure or a console
|
|
1260
|
+
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
1116
1261
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
1117
1262
|
* earlier walk → the rest ({@link traceRowTier}). Without that the note spent all five rows on four
|
|
1118
1263
|
* traces of one `afterEach` line whose every request was `-1`, and left the favorite cause — the
|
|
@@ -1136,6 +1281,15 @@ function missingApiPrefixSentence(request) {
|
|
|
1136
1281
|
* request that never reached the API. The prefix is READ (the caller passes what the app's client
|
|
1137
1282
|
* config names; the traces add what their own requests show) and never assumed to be `/api`.
|
|
1138
1283
|
*
|
|
1284
|
+
* A 401/403 on the app's own origin is answered from the request's own credential record instead
|
|
1285
|
+
* ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
|
|
1286
|
+
* token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
|
|
1287
|
+
* header the API then refused moves the answer to the server. The handler sentence is kept for
|
|
1288
|
+
* the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
|
|
1289
|
+
* trace that proves the header was never sent cannot be explained by the handler.
|
|
1290
|
+
* xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
|
|
1291
|
+
* `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
|
|
1292
|
+
*
|
|
1139
1293
|
* @param items - The traces {@link parseTraceArtifacts} read.
|
|
1140
1294
|
* @param target - Where the run went — the deployed host, as the note names it.
|
|
1141
1295
|
* @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
|
|
@@ -1186,6 +1340,11 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1186
1340
|
const prefixAdvice = missingPrefix.length > 0
|
|
1187
1341
|
? ` ${missingPrefix.slice(0, 2).map(missingApiPrefixSentence).join(' ')}`
|
|
1188
1342
|
: '';
|
|
1343
|
+
// A credential refusal (401/403 on the app's own API) is not the handler's answer: the trace's
|
|
1344
|
+
// own `Authorization` record says whether the CLIENT attached the token at all (m148 +29:18).
|
|
1345
|
+
const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null && (request.status === 401 || request.status === 403));
|
|
1346
|
+
const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
|
|
1347
|
+
const authAdvice = authRefusalAdvice(traced);
|
|
1189
1348
|
// The document subresources that never finished anywhere in this read — the assets a timed-out
|
|
1190
1349
|
// navigation was waiting on, when there is no failed request to explain the timeout.
|
|
1191
1350
|
const stalledAssets = [
|
|
@@ -1326,9 +1485,14 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1326
1485
|
(anyFailed
|
|
1327
1486
|
? everyFailureLacksPrefix
|
|
1328
1487
|
? ''
|
|
1329
|
-
:
|
|
1330
|
-
|
|
1331
|
-
|
|
1488
|
+
: everyFailureAuth
|
|
1489
|
+
? // Every failure was a credential refusal: the handler is not the problem, and the
|
|
1490
|
+
// auth advice already names what is (m148 was sent to the handler six walks running).
|
|
1491
|
+
authAdvice
|
|
1492
|
+
: 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
|
|
1493
|
+
'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
|
|
1494
|
+
'user action silently — log it.' +
|
|
1495
|
+
authAdvice
|
|
1332
1496
|
: stalledAssets.length > 0
|
|
1333
1497
|
? 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
|
|
1334
1498
|
'not finish loading because its own subresource never answered. Do not read this as a ' +
|