@molecule/api-ai-tools 1.9.11 → 1.9.13
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 +72 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +77 -0
- package/dist/trace-artifacts.d.ts +83 -7
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +343 -57
- 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-18T09:17:15.122Z
|
|
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
|
|
|
@@ -500,6 +522,52 @@ interface TraceArtifact {
|
|
|
500
522
|
}
|
|
501
523
|
```
|
|
502
524
|
|
|
525
|
+
#### `TraceRunFailure`
|
|
526
|
+
|
|
527
|
+
What the runner's own report says about ONE failing test: where the test is declared, which
|
|
528
|
+
Playwright call its failure names, and the per-test deadline the failure measured against.
|
|
529
|
+
|
|
530
|
+
A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
|
|
531
|
+
xr-molecule-flash-m153 a 15 s `page.waitForSelector` timeout left a trace whose `Frame.goto`
|
|
532
|
+
never finished (the test was cut off mid-navigation) with a `/src/components/*.tsx` module still
|
|
533
|
+
pending when the context closed — and the note read that as an asset hang, then sent the run to
|
|
534
|
+
re-run the same tests unchanged against the same dev server while the same result's
|
|
535
|
+
`testRunNote` was telling it to measure a production build.
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
interface TraceRunFailure {
|
|
539
|
+
/** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
|
|
540
|
+
spec: string
|
|
541
|
+
/** The line the test is DECLARED on, from that heading (`206`). */
|
|
542
|
+
line: number
|
|
543
|
+
/** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
|
|
544
|
+
operation: string | null
|
|
545
|
+
/** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
|
|
546
|
+
budgetMs: number | null
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
#### `TraceRunReport`
|
|
551
|
+
|
|
552
|
+
The runner's own report, beside the traces it wrote.
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
interface TraceRunReport {
|
|
556
|
+
/**
|
|
557
|
+
* One entry per failing test, as the runner printed them. Empty when the caller supplied none:
|
|
558
|
+
* without the runner's reported operation a pending asset is never called the cause.
|
|
559
|
+
*/
|
|
560
|
+
failures?: readonly TraceRunFailure[]
|
|
561
|
+
/**
|
|
562
|
+
* True when the same result's `testRunNote` already names a production build as the next
|
|
563
|
+
* measurement (a local dev-server walk that ran out of its per-test budget). The note then
|
|
564
|
+
* states the ONE next step instead of directing an unchanged same-target re-run that
|
|
565
|
+
* contradicts it.
|
|
566
|
+
*/
|
|
567
|
+
productionBuildAdvised?: boolean
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
503
571
|
#### `DiagnosisTraceRewrite`
|
|
504
572
|
|
|
505
573
|
A command with the trace flag this platform decided to add, and the sentence to report it in.
|
|
@@ -1931,7 +1999,7 @@ function stalledDocumentSubresource(request: TraceRequest): boolean
|
|
|
1931
1999
|
|
|
1932
2000
|
**Returns:** True when the request is a document subresource that never finished.
|
|
1933
2001
|
|
|
1934
|
-
#### `failureTraceNote(items, target, earlierFailures, apiPrefixes)`
|
|
2002
|
+
#### `failureTraceNote(items, target, earlierFailures, apiPrefixes, report)`
|
|
1935
2003
|
|
|
1936
2004
|
The note a failed suite run's result carries: what each failing test's page actually sent, and
|
|
1937
2005
|
what came back.
|
|
@@ -1963,6 +2031,7 @@ function failureTraceNote(
|
|
|
1963
2031
|
target: string,
|
|
1964
2032
|
earlierFailures?: readonly string[],
|
|
1965
2033
|
apiPrefixes?: readonly string[],
|
|
2034
|
+
report?: TraceRunReport,
|
|
1966
2035
|
): string | null
|
|
1967
2036
|
```
|
|
1968
2037
|
|
|
@@ -1970,6 +2039,7 @@ function failureTraceNote(
|
|
|
1970
2039
|
- `target` — Where the run went — the deployed host, as the note names it.
|
|
1971
2040
|
- `earlierFailures` — The failing `file:line`s earlier walks of this suite left, so a repeat is ranked above a one-off. Empty when the caller knows of no earlier walk.
|
|
1972
2041
|
- `apiPrefixes` — The API path prefixes the app's client config names (`['/api']`), read from the app by the caller. Empty when the caller could not read one; the traces' own requests may still supply it. Never a hard-coded literal.
|
|
2042
|
+
- `report` — The runner's own report for this run ({@link TraceRunReport}): each failing test's declared location, the operation its failure names and the deadline it names, plus whether the same result's `testRunNote` already points at a production build. A pending asset is called the cause only when the runner says the failing operation for THAT test is the navigation it was loading; without the report nothing is called a navigation stall.
|
|
1973
2043
|
|
|
1974
2044
|
**Returns:** The note, or null when no trace held a recorded response, a real failure or a console error.
|
|
1975
2045
|
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAkOD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AA2GD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAkOD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AA2GD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAokFxF"}
|
package/dist/tools.js
CHANGED
|
@@ -849,6 +849,65 @@ export function buildTools(backend, config) {
|
|
|
849
849
|
result = isEnvFilePath(path) ? redactSecrets(result) : redactSecretsInCode(result);
|
|
850
850
|
return result;
|
|
851
851
|
}
|
|
852
|
+
// ── Binary reads ───────────────────────────────────────────────
|
|
853
|
+
/** Extensions whose files are never text: a `read_file` on one is refused by name. */
|
|
854
|
+
const BINARY_FILE_EXTENSIONS = new Set([
|
|
855
|
+
'avif',
|
|
856
|
+
'gif',
|
|
857
|
+
'gz',
|
|
858
|
+
'ico',
|
|
859
|
+
'jpeg',
|
|
860
|
+
'jpg',
|
|
861
|
+
'otf',
|
|
862
|
+
'pdf',
|
|
863
|
+
'png',
|
|
864
|
+
'tgz',
|
|
865
|
+
'ttf',
|
|
866
|
+
'wasm',
|
|
867
|
+
'webp',
|
|
868
|
+
'woff',
|
|
869
|
+
'woff2',
|
|
870
|
+
'zip',
|
|
871
|
+
]);
|
|
872
|
+
/** How many chars of a read are inspected for binary markers. */
|
|
873
|
+
const BINARY_SAMPLE_CHARS = 512;
|
|
874
|
+
/**
|
|
875
|
+
* A replacement character (a byte UTF-8 could not decode) marks binary only in
|
|
876
|
+
* bulk: one literal U+FFFD in a source string is ordinary text.
|
|
877
|
+
*/
|
|
878
|
+
const BINARY_MIN_REPLACEMENTS = 8;
|
|
879
|
+
const BINARY_REPLACEMENT_DENSITY = 0.05;
|
|
880
|
+
/**
|
|
881
|
+
* The kind of a BINARY file, or `null` for text.
|
|
882
|
+
*
|
|
883
|
+
* `read_file` asks the backend for a string, so a PNG comes back as UTF-8
|
|
884
|
+
* replacement characters and used to be handed to the model whole — 21,191
|
|
885
|
+
* chars of unreadable bytes it cannot look at, ~12k tokens per read, asked for
|
|
886
|
+
* twice in one run (xr-molecule-flash-m155 +09:58). A known binary extension is
|
|
887
|
+
* refused by name; a file without one is caught from the bytes themselves, by
|
|
888
|
+
* a NUL byte or by the density of replacement characters in its first
|
|
889
|
+
* {@link BINARY_SAMPLE_CHARS} chars.
|
|
890
|
+
*
|
|
891
|
+
* @param path - The already-resolved path.
|
|
892
|
+
* @param content - The content the backend decoded as UTF-8.
|
|
893
|
+
* @returns The binary kind to name (e.g. `png`, `binary data`), or `null` for text.
|
|
894
|
+
*/
|
|
895
|
+
function binaryFileKind(path, content) {
|
|
896
|
+
const ext = posix.extname(path).slice(1).toLowerCase();
|
|
897
|
+
if (BINARY_FILE_EXTENSIONS.has(ext))
|
|
898
|
+
return ext;
|
|
899
|
+
const sample = content.slice(0, BINARY_SAMPLE_CHARS);
|
|
900
|
+
if (sample.includes('\u0000'))
|
|
901
|
+
return 'binary data';
|
|
902
|
+
let replacements = 0;
|
|
903
|
+
for (const char of sample)
|
|
904
|
+
if (char === '\uFFFD')
|
|
905
|
+
replacements += 1;
|
|
906
|
+
return replacements >= BINARY_MIN_REPLACEMENTS &&
|
|
907
|
+
replacements / sample.length >= BINARY_REPLACEMENT_DENSITY
|
|
908
|
+
? 'binary data'
|
|
909
|
+
: null;
|
|
910
|
+
}
|
|
852
911
|
// ── Empty-read verification ────────────────────────────────────
|
|
853
912
|
/**
|
|
854
913
|
* The file's size in bytes as the backend's own shell reports it, or `null`
|
|
@@ -1022,6 +1081,19 @@ export function buildTools(backend, config) {
|
|
|
1022
1081
|
...(verdict.note ? { note: verdict.note } : {}),
|
|
1023
1082
|
});
|
|
1024
1083
|
}
|
|
1084
|
+
// A binary file decoded as UTF-8 is unreadable by definition — say what it
|
|
1085
|
+
// is instead of returning the bytes (see binaryFileKind).
|
|
1086
|
+
const binaryKind = binaryFileKind(path, content);
|
|
1087
|
+
if (binaryKind) {
|
|
1088
|
+
// The read already happened, so the decoded length is the fallback size;
|
|
1089
|
+
// `wc -c` gives the file's real byte count when the backend can run it.
|
|
1090
|
+
const bytes = (await probeFileSize(path)) ?? content.length;
|
|
1091
|
+
return {
|
|
1092
|
+
error: `${path} is a binary file (${binaryKind}, ${Math.max(1, Math.round(bytes / 1024))} KB). ` +
|
|
1093
|
+
`read_file returns text only — to look at a page use screenshot_preview; to inspect an ` +
|
|
1094
|
+
`image you produced, measure it in a script and print the numbers (its size, pixel counts).`,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1025
1097
|
if (content.length > MAX_READ_SIZE)
|
|
1026
1098
|
return {
|
|
1027
1099
|
error: `File too large (${Math.round(content.length / 1024)}KB). Maximum is ${MAX_READ_SIZE / 1024 / 1024}MB.`,
|
|
@@ -2911,8 +2983,13 @@ export function buildTools(backend, config) {
|
|
|
2911
2983
|
: { piped: false, unplaced: false };
|
|
2912
2984
|
const reported = suiteRunExitCode(exitCode, output, placement.piped);
|
|
2913
2985
|
const noCodeNote = suiteExitCodeNote(placement, reported);
|
|
2986
|
+
// A FINISHED result says so, even when the code it would report is withheld: a filtered
|
|
2987
|
+
// pipeline's code is not the run's verdict ({@link FILTERED_EXIT_NOTE}), so `exitCode` alone
|
|
2988
|
+
// cannot tell a caller this is the delivered result rather than a progress return — and a
|
|
2989
|
+
// caller that tracks background tasks would hand the same result back as fresh news later.
|
|
2914
2990
|
return {
|
|
2915
2991
|
taskId,
|
|
2992
|
+
status: 'completed',
|
|
2916
2993
|
exitCode: reported,
|
|
2917
2994
|
waitedMs,
|
|
2918
2995
|
...(causes || noCodeNote
|
|
@@ -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.
|
|
@@ -257,6 +279,42 @@ export declare function unansweredRequest(request: TraceRequest): boolean;
|
|
|
257
279
|
* @returns True when the request is a document subresource that never finished.
|
|
258
280
|
*/
|
|
259
281
|
export declare function stalledDocumentSubresource(request: TraceRequest): boolean;
|
|
282
|
+
/**
|
|
283
|
+
* What the runner's own report says about ONE failing test: where the test is declared, which
|
|
284
|
+
* Playwright call its failure names, and the per-test deadline the failure measured against.
|
|
285
|
+
*
|
|
286
|
+
* A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
|
|
287
|
+
* xr-molecule-flash-m153 a 15 s `page.waitForSelector` timeout left a trace whose `Frame.goto`
|
|
288
|
+
* never finished (the test was cut off mid-navigation) with a `/src/components/*.tsx` module still
|
|
289
|
+
* pending when the context closed — and the note read that as an asset hang, then sent the run to
|
|
290
|
+
* re-run the same tests unchanged against the same dev server while the same result's
|
|
291
|
+
* `testRunNote` was telling it to measure a production build.
|
|
292
|
+
*/
|
|
293
|
+
export interface TraceRunFailure {
|
|
294
|
+
/** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
|
|
295
|
+
spec: string;
|
|
296
|
+
/** The line the test is DECLARED on, from that heading (`206`). */
|
|
297
|
+
line: number;
|
|
298
|
+
/** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
|
|
299
|
+
operation: string | null;
|
|
300
|
+
/** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
|
|
301
|
+
budgetMs: number | null;
|
|
302
|
+
}
|
|
303
|
+
/** The runner's own report, beside the traces it wrote. */
|
|
304
|
+
export interface TraceRunReport {
|
|
305
|
+
/**
|
|
306
|
+
* One entry per failing test, as the runner printed them. Empty when the caller supplied none:
|
|
307
|
+
* without the runner's reported operation a pending asset is never called the cause.
|
|
308
|
+
*/
|
|
309
|
+
failures?: readonly TraceRunFailure[];
|
|
310
|
+
/**
|
|
311
|
+
* True when the same result's `testRunNote` already names a production build as the next
|
|
312
|
+
* measurement (a local dev-server walk that ran out of its per-test budget). The note then
|
|
313
|
+
* states the ONE next step instead of directing an unchanged same-target re-run that
|
|
314
|
+
* contradicts it.
|
|
315
|
+
*/
|
|
316
|
+
productionBuildAdvised?: boolean;
|
|
317
|
+
}
|
|
260
318
|
/**
|
|
261
319
|
* The note a failed suite run's result carries: what each failing test's page actually sent, and
|
|
262
320
|
* what came back.
|
|
@@ -299,12 +357,16 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
299
357
|
* transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
|
|
300
358
|
* rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
|
|
301
359
|
*
|
|
302
|
-
* When a navigation never finished and one of the document's
|
|
303
|
-
* script, an asset under `/assets/`) never got a response either
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* (`/sw.js`, the manifest, a
|
|
360
|
+
* When a navigation the runner reports as the failure never finished and one of the document's
|
|
361
|
+
* own subresources (a stylesheet, a script, an asset under `/assets/`) never got a response either
|
|
362
|
+
* — having started inside that wait — the request takes a row and the note says the page was
|
|
363
|
+
* waiting on it. It is not an API call, but on a `page.goto` timeout it is the only request that
|
|
364
|
+
* can explain the timeout — the deployed app was serving the page while one fetch stalled for 27 s
|
|
365
|
+
* (xr-molecule-flash-m135 +34:10). The page shell's incidental requests (`/sw.js`, the manifest, a
|
|
366
|
+
* favicon, a font) stay out, for m125's reason. An asset merely unfinished when a selector wait
|
|
367
|
+
* ran out the test budget, or one a teardown/`afterEach` trace holds, is not a cause: the note
|
|
368
|
+
* says it "had no recorded response when the test ended; that alone does not establish the cause"
|
|
369
|
+
* (xr-molecule-flash-m153 +13:43).
|
|
308
370
|
*
|
|
309
371
|
* A 404 whose path lacks the API prefix the app's own calls carry is called out with that prefix
|
|
310
372
|
* ({@link missingApiPrefixRequests}): xr-molecule-flash-m145 posted `/users` to `vite preview`,
|
|
@@ -313,6 +375,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
313
375
|
* request that never reached the API. The prefix is READ (the caller passes what the app's client
|
|
314
376
|
* config names; the traces add what their own requests show) and never assumed to be `/api`.
|
|
315
377
|
*
|
|
378
|
+
* A 401/403 on the app's own origin is answered from the request's own credential record instead
|
|
379
|
+
* ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
|
|
380
|
+
* token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
|
|
381
|
+
* header the API then refused moves the answer to the server. The handler sentence is kept for
|
|
382
|
+
* the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
|
|
383
|
+
* trace that proves the header was never sent cannot be explained by the handler.
|
|
384
|
+
* xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
|
|
385
|
+
* `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
|
|
386
|
+
*
|
|
316
387
|
* @param items - The traces {@link parseTraceArtifacts} read.
|
|
317
388
|
* @param target - Where the run went — the deployed host, as the note names it.
|
|
318
389
|
* @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
|
|
@@ -320,10 +391,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
320
391
|
* @param apiPrefixes - The API path prefixes the app's client config names (`['/api']`), read from
|
|
321
392
|
* the app by the caller. Empty when the caller could not read one; the traces' own requests may
|
|
322
393
|
* still supply it. Never a hard-coded literal.
|
|
394
|
+
* @param report - The runner's own report for this run ({@link TraceRunReport}): each failing
|
|
395
|
+
* test's declared location, the operation its failure names and the deadline it names, plus
|
|
396
|
+
* whether the same result's `testRunNote` already points at a production build. A pending asset
|
|
397
|
+
* is called the cause only when the runner says the failing operation for THAT test is the
|
|
398
|
+
* navigation it was loading; without the report nothing is called a navigation stall.
|
|
323
399
|
* @returns The note, or null when no trace held a recorded response, a real failure or a console
|
|
324
400
|
* error.
|
|
325
401
|
*/
|
|
326
|
-
export declare function failureTraceNote(items: readonly TraceArtifact[], target: string, earlierFailures?: readonly string[], apiPrefixes?: readonly string[]): string | null;
|
|
402
|
+
export declare function failureTraceNote(items: readonly TraceArtifact[], target: string, earlierFailures?: readonly string[], apiPrefixes?: readonly string[], report?: TraceRunReport): string | null;
|
|
327
403
|
/**
|
|
328
404
|
* Whether a Playwright config records traces for the tests that fail.
|
|
329
405
|
*
|
|
@@ -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
|
@@ -243,12 +243,34 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
243
243
|
const response = snapshot.response || {}
|
|
244
244
|
const type = String(snapshot._resourceType || '')
|
|
245
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
|
+
})
|
|
246
265
|
requests.push({
|
|
247
266
|
method: String(request.method).toUpperCase(),
|
|
248
267
|
path: String(request.url).slice(0, MAX_URL),
|
|
249
268
|
status: Number(response.status) || 0,
|
|
250
269
|
failure: typeof response._failureText === 'string' ? response._failureText : undefined,
|
|
251
270
|
type: type,
|
|
271
|
+
sentAuthorization: authHeader !== undefined,
|
|
272
|
+
authorization: authScheme,
|
|
273
|
+
sentCookie: sentCookie,
|
|
252
274
|
// The HAR entry carries the monotonic clock the trace's action entries use, so a request
|
|
253
275
|
// can be attributed to the action that caused it (see \`lastAction\` below).
|
|
254
276
|
startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
|
|
@@ -430,6 +452,12 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
430
452
|
...(r.sent ? { sent: r.sent } : {}),
|
|
431
453
|
...(r.got ? { got: r.got } : {}),
|
|
432
454
|
type: r.type || undefined,
|
|
455
|
+
// The trace's own clock, so the note can tell a request the navigation was waiting on from
|
|
456
|
+
// one that was already pending before it started (m135/m153).
|
|
457
|
+
...(typeof r.startedAt === 'number' ? { startedAt: r.startedAt } : {}),
|
|
458
|
+
...(r.sentAuthorization ? { sentAuthorization: true } : {}),
|
|
459
|
+
...(r.authorization ? { authorization: r.authorization } : {}),
|
|
460
|
+
...(r.sentCookie ? { sentCookie: true } : {}),
|
|
433
461
|
}
|
|
434
462
|
}
|
|
435
463
|
// The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
|
|
@@ -590,6 +618,12 @@ function parseTraceRequest(value) {
|
|
|
590
618
|
...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
|
|
591
619
|
...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
|
|
592
620
|
...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
|
|
621
|
+
// The scheme word only: a raw header value is a live credential and is never parsed in.
|
|
622
|
+
...(r.sentAuthorization === true ? { sentAuthorization: true } : {}),
|
|
623
|
+
...(typeof r.authorization === 'string' && r.authorization
|
|
624
|
+
? { authorization: r.authorization }
|
|
625
|
+
: {}),
|
|
626
|
+
...(r.sentCookie === true ? { sentCookie: true } : {}),
|
|
593
627
|
};
|
|
594
628
|
}
|
|
595
629
|
/**
|
|
@@ -934,6 +968,97 @@ function repeatsEarlierFailure(item, earlier) {
|
|
|
934
968
|
}
|
|
935
969
|
return false;
|
|
936
970
|
}
|
|
971
|
+
/** The Playwright navigation calls whose wait a pending document subresource can explain. */
|
|
972
|
+
const NAVIGATION_OPERATIONS = new Set(['goto', 'waitForURL', 'waitForLoadState', 'reload']);
|
|
973
|
+
/**
|
|
974
|
+
* The method a runner-reported operation names (`page.waitForSelector` → `waitForSelector`), or
|
|
975
|
+
* null when it is not one of the navigation calls a stalled asset can explain.
|
|
976
|
+
*
|
|
977
|
+
* @param operation - The call the runner's error text names, or null.
|
|
978
|
+
* @returns The navigation method, or null.
|
|
979
|
+
*/
|
|
980
|
+
function navigationMethod(operation) {
|
|
981
|
+
if (operation === null)
|
|
982
|
+
return null;
|
|
983
|
+
const method = operation.slice(operation.lastIndexOf('.') + 1);
|
|
984
|
+
return NAVIGATION_OPERATIONS.has(method) ? method : null;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* The runner's report for the test THIS trace belongs to, or undefined when it belongs to none.
|
|
988
|
+
*
|
|
989
|
+
* The trace names the line of its LAST recorded step; the runner's summary names the test's
|
|
990
|
+
* DECLARATION line, and the step sits inside the body just after it ({@link
|
|
991
|
+
* RECURRENCE_LINE_WINDOW}). A line BEFORE the declaration is a hook — m153's teardown trace sits
|
|
992
|
+
* at `url-navigation.spec.ts:107`, the Pagination suite's `afterEach`, while the failing test is
|
|
993
|
+
* declared at `:206` — and a hook is not the failing test's operation.
|
|
994
|
+
*
|
|
995
|
+
* @param item - One traced failure.
|
|
996
|
+
* @param failures - The runner's report, one entry per failing test.
|
|
997
|
+
* @returns The matching failure, or undefined.
|
|
998
|
+
*/
|
|
999
|
+
function runFailureOf(item, failures) {
|
|
1000
|
+
if (item.spec === null)
|
|
1001
|
+
return undefined;
|
|
1002
|
+
for (const failure of failures) {
|
|
1003
|
+
if (!sameLocationFile(item.spec, failure.spec))
|
|
1004
|
+
continue;
|
|
1005
|
+
if (item.line < failure.line)
|
|
1006
|
+
continue;
|
|
1007
|
+
if (item.line - failure.line <= RECURRENCE_LINE_WINDOW)
|
|
1008
|
+
return failure;
|
|
1009
|
+
}
|
|
1010
|
+
return undefined;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Whether a pending request's interval covers a navigation's wait: it started while the
|
|
1014
|
+
* navigation was loading (at or after the wait began) and it has no recorded response, so it was
|
|
1015
|
+
* still pending when the wait ran out. A request that started before the wait belongs to the page
|
|
1016
|
+
* already on screen, not to the navigation that never finished.
|
|
1017
|
+
*
|
|
1018
|
+
* @param request - One request the trace recorded.
|
|
1019
|
+
* @param wait - The navigation action the runner reported.
|
|
1020
|
+
* @returns True when the request's pending interval covers the wait.
|
|
1021
|
+
*/
|
|
1022
|
+
function requestCoversWait(request, wait) {
|
|
1023
|
+
if (typeof request.startedAt !== 'number')
|
|
1024
|
+
return false;
|
|
1025
|
+
return request.startedAt >= wait.start - 1;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* The stalled-navigation reading for one trace — or null.
|
|
1029
|
+
*
|
|
1030
|
+
* All three facts must hold together: the runner says THIS test's failing operation is a
|
|
1031
|
+
* navigation ({@link NAVIGATION_OPERATIONS}); the trace's record of that call never finished; and
|
|
1032
|
+
* a document subresource still pending at the end started inside that wait
|
|
1033
|
+
* ({@link requestCoversWait}). An asset merely unfinished when the test was cancelled, or one a
|
|
1034
|
+
* teardown/`afterEach` trace holds, meets none of these and is not a cause (m153).
|
|
1035
|
+
*
|
|
1036
|
+
* @param item - One traced failure.
|
|
1037
|
+
* @param report - The runner's report and the run's next step.
|
|
1038
|
+
* @returns The wait and the assets it was waiting on, or null.
|
|
1039
|
+
*/
|
|
1040
|
+
function stalledNavigation(item, report) {
|
|
1041
|
+
const failure = runFailureOf(item, report.failures ?? []);
|
|
1042
|
+
if (failure === undefined)
|
|
1043
|
+
return null;
|
|
1044
|
+
const method = navigationMethod(failure.operation);
|
|
1045
|
+
if (method === null)
|
|
1046
|
+
return null;
|
|
1047
|
+
let wait = null;
|
|
1048
|
+
for (const action of item.actions) {
|
|
1049
|
+
if (!action.api.endsWith(`.${method}`))
|
|
1050
|
+
continue;
|
|
1051
|
+
if (wait === null || action.start > wait.start)
|
|
1052
|
+
wait = action;
|
|
1053
|
+
}
|
|
1054
|
+
// The navigation is the failure only while it is still running at the end of the test.
|
|
1055
|
+
if (wait === null || wait.end !== null)
|
|
1056
|
+
return null;
|
|
1057
|
+
const assets = item.requests.filter((request) => stalledDocumentSubresource(request) && requestCoversWait(request, wait));
|
|
1058
|
+
if (assets.length === 0)
|
|
1059
|
+
return null;
|
|
1060
|
+
return { reported: failure.operation ?? wait.api, wait, assets, budgetMs: failure.budgetMs };
|
|
1061
|
+
}
|
|
937
1062
|
/**
|
|
938
1063
|
* Whether a trace holds evidence the note can explain: a request that got a RECORDED response, a
|
|
939
1064
|
* real failure (4xx/5xx or a network error), or a console line. A request with no response is
|
|
@@ -943,31 +1068,38 @@ function repeatsEarlierFailure(item, earlier) {
|
|
|
943
1068
|
*
|
|
944
1069
|
* A request the page shell made (m125 +29:33: `/sw.js`, a font) is not evidence about the step
|
|
945
1070
|
* either, answered or not. The exception is a document subresource that never finished while the
|
|
946
|
-
*
|
|
947
|
-
* `page.goto` timed out ({@link
|
|
1071
|
+
* navigation the RUNNER reports failing was still open: that navigation waits on it, so it is the
|
|
1072
|
+
* reason the `page.goto` timed out ({@link stalledNavigation}).
|
|
948
1073
|
*
|
|
949
1074
|
* @param item - One traced failure.
|
|
1075
|
+
* @param report - The runner's report and the run's next step.
|
|
950
1076
|
* @returns True when the trace holds a request or console line worth a row.
|
|
951
1077
|
*/
|
|
952
|
-
function traceRowEvidence(item) {
|
|
1078
|
+
function traceRowEvidence(item, report) {
|
|
953
1079
|
return (item.console.length > 0 ||
|
|
954
1080
|
item.requests.some((request) => !isStaticAssetRequest(request) && !unansweredRequest(request)) ||
|
|
955
|
-
// The one shape in which a subresource with no response IS the cause: the navigation
|
|
956
|
-
//
|
|
957
|
-
(
|
|
1081
|
+
// The one shape in which a subresource with no response IS the cause: the navigation the
|
|
1082
|
+
// runner reported was still running at the end (see {@link stalledNavigation}).
|
|
1083
|
+
stalledNavigation(item, report) !== null);
|
|
958
1084
|
}
|
|
959
1085
|
/**
|
|
960
1086
|
* The note's row order: a row with a REAL failure first, then a row whose failure an earlier walk
|
|
961
1087
|
* also had, then the rest. A recurrence is the only evidence a row carries when its requests were
|
|
962
1088
|
* all answered (m112's favorite cause: the click's own request answered, the button never
|
|
963
1089
|
* changed).
|
|
1090
|
+
*
|
|
1091
|
+
* @param item - One traced failure.
|
|
1092
|
+
* @param earlier - The failing `file:line`s earlier walks of this suite left.
|
|
1093
|
+
* @param report - The runner's report and the run's next step.
|
|
1094
|
+
* @returns The row's tier: 0 first, 2 last.
|
|
964
1095
|
*/
|
|
965
|
-
function traceRowTier(item, earlier) {
|
|
1096
|
+
function traceRowTier(item, earlier, report) {
|
|
966
1097
|
if (item.requests.some(failedRequest))
|
|
967
1098
|
return 0;
|
|
968
|
-
// A navigation
|
|
969
|
-
// strong a cause as a failed request — it is the one fact the timeout was waiting on
|
|
970
|
-
|
|
1099
|
+
// A navigation the runner reported, still waiting on a subresource that never got a response,
|
|
1100
|
+
// is as strong a cause as a failed request — it is the one fact the timeout was waiting on
|
|
1101
|
+
// (m135).
|
|
1102
|
+
if (stalledNavigation(item, report) !== null)
|
|
971
1103
|
return 0;
|
|
972
1104
|
if (repeatsEarlierFailure(item, earlier))
|
|
973
1105
|
return 1;
|
|
@@ -1031,6 +1163,94 @@ function sameOriginPath(request) {
|
|
|
1031
1163
|
return null;
|
|
1032
1164
|
return raw.replace(/\/+$/, '');
|
|
1033
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* The sentence a 401/403 on the app's own API gets: which half of the wire refused the request.
|
|
1168
|
+
*
|
|
1169
|
+
* A fixed "read the handler this route reaches" line is the wrong half for a credential refusal.
|
|
1170
|
+
* The request's own `Authorization` header — recorded by the trace reader as its scheme WORD, never
|
|
1171
|
+
* its value — decides: absent means the CLIENT never attached the token (a session store that the
|
|
1172
|
+
* HTTP client does not read, a bare `fetch`, an auth bond the app replaced), while present means
|
|
1173
|
+
* the SERVER refused a token it was sent (scheme or issuer). A 403 is a permission answer, not a
|
|
1174
|
+
* credential one, and says so.
|
|
1175
|
+
*
|
|
1176
|
+
* xr-molecule-flash-m148 (+29:18): `POST /api/articles/…/favorite → 401 {"errors":{"token":["is
|
|
1177
|
+
* missing"]}}` was answered with the handler sentence for six local walks, while the trace proved
|
|
1178
|
+
* the request carried no `Authorization` header at all and the app's hand-rolled session store
|
|
1179
|
+
* never handed its token to the shared client.
|
|
1180
|
+
*
|
|
1181
|
+
* The trace cannot name the file that made the call: a real HAR entry carries the request, response,
|
|
1182
|
+
* `_resourceType`, `_monotonicTime` and a frame ref, but no initiator stack. So the sentence names
|
|
1183
|
+
* the shared client and the auth bond instead of guessing a file.
|
|
1184
|
+
*
|
|
1185
|
+
* @param request - The refused request, a 401 or 403 on the app's own origin.
|
|
1186
|
+
* @returns The sentence the note appends.
|
|
1187
|
+
*/
|
|
1188
|
+
function authRefusalSentence(request) {
|
|
1189
|
+
const label = requestLabel(request);
|
|
1190
|
+
// The header's scheme, when it parsed; presence alone is still the server's half of the wire.
|
|
1191
|
+
const carried = request.authorization !== undefined
|
|
1192
|
+
? `with \`Authorization: ${request.authorization} …\``
|
|
1193
|
+
: 'with an `Authorization` header';
|
|
1194
|
+
if (request.status === 403) {
|
|
1195
|
+
return request.sentAuthorization === true
|
|
1196
|
+
? `The browser sent \`${label}\` ${carried} and the API answered 403 — the token was ` +
|
|
1197
|
+
'accepted but this user may not do it: read the route’s role/permission check in your ' +
|
|
1198
|
+
'own source, and the account the session belongs to.'
|
|
1199
|
+
: `The browser sent \`${label}\` and the API answered 403 — the request carried no ` +
|
|
1200
|
+
'`Authorization` header, so either the client did not attach the token (check that the ' +
|
|
1201
|
+
'request goes through the app’s HTTP client that the auth bond configures, and that the ' +
|
|
1202
|
+
'token is stored where the client reads it) or the route refuses this user: read the ' +
|
|
1203
|
+
'route’s role/permission check in your own source.';
|
|
1204
|
+
}
|
|
1205
|
+
if (request.sentAuthorization === true) {
|
|
1206
|
+
return (`The browser sent \`${label}\` ${carried} and the API refused it: the token was sent and ` +
|
|
1207
|
+
'rejected — the scheme (the client’s `authScheme`, e.g. `Token` vs `Bearer`) or the ' +
|
|
1208
|
+
'token’s issuer differs from what the server verifies; read the verify middleware this ' +
|
|
1209
|
+
'route reaches.');
|
|
1210
|
+
}
|
|
1211
|
+
return (`The browser sent \`${label}\` with NO \`Authorization\` header — the client did not attach ` +
|
|
1212
|
+
'the token: check that the request goes through the app’s HTTP client that the auth bond ' +
|
|
1213
|
+
'configures (not a bare `fetch`), and that the token is stored where the client reads it. ' +
|
|
1214
|
+
'The shared client carries it through `setAuthToken` — the auth bond ' +
|
|
1215
|
+
'(`createDefaultAuthClientWithHttpSync` in `@molecule/app-bonds-default-react`) mirrors the ' +
|
|
1216
|
+
'auth client’s token there, and a session store of your own has to call it too. ' +
|
|
1217
|
+
(request.sentCookie
|
|
1218
|
+
? 'The request did carry a `Cookie` header, so if this app signs in with a session cookie ' +
|
|
1219
|
+
'that is the credential the server did not accept. '
|
|
1220
|
+
: '') +
|
|
1221
|
+
'The API handler is not the problem.');
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* The note's credential-refusal advice, one sentence per distinct shape — at most two.
|
|
1225
|
+
*
|
|
1226
|
+
* The shapes are `(status, Authorization scheme, cookie present)`: m148's ten walks repeated one
|
|
1227
|
+
* shape (`401`, no header) on every failing test, and repeating its sentence per row would bury
|
|
1228
|
+
* the fact rather than state it.
|
|
1229
|
+
*
|
|
1230
|
+
* @param items - The traces the note is about.
|
|
1231
|
+
* @returns The sentences to append (each led by a space), or '' when no same-origin 401/403 was
|
|
1232
|
+
* traced.
|
|
1233
|
+
*/
|
|
1234
|
+
function authRefusalAdvice(items) {
|
|
1235
|
+
const sentences = [];
|
|
1236
|
+
const seen = new Set();
|
|
1237
|
+
for (const item of items) {
|
|
1238
|
+
for (const request of item.requests) {
|
|
1239
|
+
if (request.status !== 401 && request.status !== 403)
|
|
1240
|
+
continue;
|
|
1241
|
+
if (sameOriginPath(request) === null)
|
|
1242
|
+
continue;
|
|
1243
|
+
const key = `${request.status} ${request.sentAuthorization ? (request.authorization ?? 'header') : 'none'} ${request.sentCookie ? 'cookie' : ''}`;
|
|
1244
|
+
if (seen.has(key))
|
|
1245
|
+
continue;
|
|
1246
|
+
seen.add(key);
|
|
1247
|
+
sentences.push(authRefusalSentence(request));
|
|
1248
|
+
if (sentences.length === 2)
|
|
1249
|
+
return ` ${sentences.join(' ')}`;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return sentences.length === 0 ? '' : ` ${sentences.join(' ')}`;
|
|
1253
|
+
}
|
|
1034
1254
|
/**
|
|
1035
1255
|
* The API path prefixes the traced requests themselves show.
|
|
1036
1256
|
*
|
|
@@ -1148,12 +1368,16 @@ function missingApiPrefixSentence(request) {
|
|
|
1148
1368
|
* transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
|
|
1149
1369
|
* rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
|
|
1150
1370
|
*
|
|
1151
|
-
* When a navigation never finished and one of the document's
|
|
1152
|
-
* script, an asset under `/assets/`) never got a response either
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
* (`/sw.js`, the manifest, a
|
|
1371
|
+
* When a navigation the runner reports as the failure never finished and one of the document's
|
|
1372
|
+
* own subresources (a stylesheet, a script, an asset under `/assets/`) never got a response either
|
|
1373
|
+
* — having started inside that wait — the request takes a row and the note says the page was
|
|
1374
|
+
* waiting on it. It is not an API call, but on a `page.goto` timeout it is the only request that
|
|
1375
|
+
* can explain the timeout — the deployed app was serving the page while one fetch stalled for 27 s
|
|
1376
|
+
* (xr-molecule-flash-m135 +34:10). The page shell's incidental requests (`/sw.js`, the manifest, a
|
|
1377
|
+
* favicon, a font) stay out, for m125's reason. An asset merely unfinished when a selector wait
|
|
1378
|
+
* ran out the test budget, or one a teardown/`afterEach` trace holds, is not a cause: the note
|
|
1379
|
+
* says it "had no recorded response when the test ended; that alone does not establish the cause"
|
|
1380
|
+
* (xr-molecule-flash-m153 +13:43).
|
|
1157
1381
|
*
|
|
1158
1382
|
* A 404 whose path lacks the API prefix the app's own calls carry is called out with that prefix
|
|
1159
1383
|
* ({@link missingApiPrefixRequests}): xr-molecule-flash-m145 posted `/users` to `vite preview`,
|
|
@@ -1162,6 +1386,15 @@ function missingApiPrefixSentence(request) {
|
|
|
1162
1386
|
* request that never reached the API. The prefix is READ (the caller passes what the app's client
|
|
1163
1387
|
* config names; the traces add what their own requests show) and never assumed to be `/api`.
|
|
1164
1388
|
*
|
|
1389
|
+
* A 401/403 on the app's own origin is answered from the request's own credential record instead
|
|
1390
|
+
* ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
|
|
1391
|
+
* token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
|
|
1392
|
+
* header the API then refused moves the answer to the server. The handler sentence is kept for
|
|
1393
|
+
* the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
|
|
1394
|
+
* trace that proves the header was never sent cannot be explained by the handler.
|
|
1395
|
+
* xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
|
|
1396
|
+
* `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
|
|
1397
|
+
*
|
|
1165
1398
|
* @param items - The traces {@link parseTraceArtifacts} read.
|
|
1166
1399
|
* @param target - Where the run went — the deployed host, as the note names it.
|
|
1167
1400
|
* @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
|
|
@@ -1169,17 +1402,22 @@ function missingApiPrefixSentence(request) {
|
|
|
1169
1402
|
* @param apiPrefixes - The API path prefixes the app's client config names (`['/api']`), read from
|
|
1170
1403
|
* the app by the caller. Empty when the caller could not read one; the traces' own requests may
|
|
1171
1404
|
* still supply it. Never a hard-coded literal.
|
|
1405
|
+
* @param report - The runner's own report for this run ({@link TraceRunReport}): each failing
|
|
1406
|
+
* test's declared location, the operation its failure names and the deadline it names, plus
|
|
1407
|
+
* whether the same result's `testRunNote` already points at a production build. A pending asset
|
|
1408
|
+
* is called the cause only when the runner says the failing operation for THAT test is the
|
|
1409
|
+
* navigation it was loading; without the report nothing is called a navigation stall.
|
|
1172
1410
|
* @returns The note, or null when no trace held a recorded response, a real failure or a console
|
|
1173
1411
|
* error.
|
|
1174
1412
|
*/
|
|
1175
|
-
export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = []) {
|
|
1176
|
-
const candidates = items.filter(traceRowEvidence);
|
|
1413
|
+
export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = [], report = {}) {
|
|
1414
|
+
const candidates = items.filter((item) => traceRowEvidence(item, report));
|
|
1177
1415
|
if (candidates.length === 0)
|
|
1178
1416
|
return null;
|
|
1179
1417
|
// Ranked, then deduplicated BY LOCATION — in that order, so the row kept for a location is the
|
|
1180
1418
|
// one carrying the strongest evidence (m112's duplicate `afterEach` rows).
|
|
1181
1419
|
const ranked = candidates
|
|
1182
|
-
.map((item, index) => ({ item, index, tier: traceRowTier(item, earlierFailures) }))
|
|
1420
|
+
.map((item, index) => ({ item, index, tier: traceRowTier(item, earlierFailures, report) }))
|
|
1183
1421
|
.sort((left, right) => left.tier - right.tier || left.index - right.index);
|
|
1184
1422
|
const traced = [];
|
|
1185
1423
|
const seen = new Set();
|
|
@@ -1212,14 +1450,25 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1212
1450
|
const prefixAdvice = missingPrefix.length > 0
|
|
1213
1451
|
? ` ${missingPrefix.slice(0, 2).map(missingApiPrefixSentence).join(' ')}`
|
|
1214
1452
|
: '';
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1453
|
+
// A credential refusal (401/403 on the app's own API) is not the handler's answer: the trace's
|
|
1454
|
+
// own `Authorization` record says whether the CLIENT attached the token at all (m148 +29:18).
|
|
1455
|
+
const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null && (request.status === 401 || request.status === 403));
|
|
1456
|
+
const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
|
|
1457
|
+
const authAdvice = authRefusalAdvice(traced);
|
|
1458
|
+
// The stalled-navigation reading, per traced test: non-null only when the RUNNER says this
|
|
1459
|
+
// test's failing operation is a navigation and a document subresource still pending at the end
|
|
1460
|
+
// started inside that wait ({@link stalledNavigation}).
|
|
1461
|
+
const stalls = traced.map((item) => stalledNavigation(item, report));
|
|
1462
|
+
// "Every failure this run traced…" is only honest when every traced test meets that bar — one
|
|
1463
|
+
// stalled trace beside a selector-wait failure is not the cause of the run (m153).
|
|
1464
|
+
const everyTracedStalls = stalls.every((stall) => stall !== null);
|
|
1465
|
+
const stalledAssets = everyTracedStalls
|
|
1466
|
+
? [...new Set(stalls.flatMap((stall) => stall?.assets.map(requestLabel) ?? []))]
|
|
1467
|
+
: [];
|
|
1468
|
+
const stalledWaits = [
|
|
1469
|
+
...new Set(stalls.flatMap((stall) => (stall === null ? [] : [stall.reported]))),
|
|
1222
1470
|
];
|
|
1471
|
+
const anyStalled = stalls.some((stall) => stall !== null);
|
|
1223
1472
|
const rows = [];
|
|
1224
1473
|
for (const [index, item] of traced.entries()) {
|
|
1225
1474
|
// The test's LAST action before the failing assertion leads the row (m148): the requests it
|
|
@@ -1227,10 +1476,24 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1227
1476
|
// page's own last call (a redirect target's `GET /api/tags`) and the shell's files do not.
|
|
1228
1477
|
const lead = item.lead;
|
|
1229
1478
|
const step = item.lastAction;
|
|
1230
|
-
// A navigation
|
|
1231
|
-
//
|
|
1232
|
-
const
|
|
1233
|
-
const
|
|
1479
|
+
// A navigation the runner reported, still waiting on a stalled asset (m135), leads the row
|
|
1480
|
+
// instead of the action's own document request.
|
|
1481
|
+
const stall = stalls[index] ?? null;
|
|
1482
|
+
const stalledNav = stall !== null;
|
|
1483
|
+
// The document subresources that never finished inside the navigation the runner reported:
|
|
1484
|
+
// the page was waiting on them when the test's budget ran out (m135).
|
|
1485
|
+
const stalled = stall?.assets ?? [];
|
|
1486
|
+
// The same kind of pending asset on a trace the runner does NOT report as a navigation
|
|
1487
|
+
// failure (a selector wait that ran out the test budget, an `afterEach`/teardown trace): it is
|
|
1488
|
+
// not a cause, and it is still worth naming so the executor does not have to guess (m153).
|
|
1489
|
+
const stalledKeys = new Set(stalled.map(requestKey));
|
|
1490
|
+
const pendingAssets = item.requests.filter((request) => stalledDocumentSubresource(request) && !stalledKeys.has(requestKey(request)));
|
|
1491
|
+
const pendingKeys = new Set(pendingAssets.map(requestKey));
|
|
1492
|
+
// A pending asset says nothing about what the action got back, so it never rides the action
|
|
1493
|
+
// lead — the honest bucket below names it (m153).
|
|
1494
|
+
const stepRequests = step !== undefined && !stalledNav
|
|
1495
|
+
? step.requests.filter((request) => !pendingKeys.has(requestKey(request)))
|
|
1496
|
+
: [];
|
|
1234
1497
|
const stepKeys = new Set(stepRequests.map(requestKey));
|
|
1235
1498
|
const isLead = (request) => lead !== undefined && requestKey(request) === requestKey(lead);
|
|
1236
1499
|
// The page shell's own requests take no row, in any bucket: an aborted `/sw.js` would
|
|
@@ -1239,20 +1502,16 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1239
1502
|
const failed = requests.filter(failedRequest);
|
|
1240
1503
|
const unanswered = requests.filter(unansweredRequest);
|
|
1241
1504
|
const answered = requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
|
|
1242
|
-
|
|
1243
|
-
// the page was waiting on it when the test's budget ran out (m135).
|
|
1244
|
-
const stalled = navigationNeverFinished(item)
|
|
1245
|
-
? item.requests.filter(stalledDocumentSubresource)
|
|
1246
|
-
: [];
|
|
1505
|
+
const budget = stall?.budgetMs ?? null;
|
|
1247
1506
|
let room = MAX_TRACE_NOTE_REQUESTS;
|
|
1248
1507
|
const parts = [];
|
|
1249
1508
|
if (step !== undefined && !stalledNav) {
|
|
1250
1509
|
const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
|
|
1251
|
-
if (
|
|
1510
|
+
if (stepRequests.length === 0) {
|
|
1252
1511
|
parts.push(`${head}, and it sent no request`);
|
|
1253
1512
|
}
|
|
1254
1513
|
else {
|
|
1255
|
-
const shown =
|
|
1514
|
+
const shown = stepRequests.slice(0, room);
|
|
1256
1515
|
room -= shown.length;
|
|
1257
1516
|
// The response body is the point (m148: the register `POST /api/users` answered 201 and
|
|
1258
1517
|
// the page rendered signed-out anyway) — printed whatever the status.
|
|
@@ -1279,18 +1538,25 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1279
1538
|
const shown = stalled.slice(0, room);
|
|
1280
1539
|
room -= shown.length;
|
|
1281
1540
|
parts.push(`never finished: ${shown.map(requestLabel).join('; ')} — a document subresource with no ` +
|
|
1282
|
-
|
|
1541
|
+
`response, still in flight when the ${stall?.reported ?? 'navigation'} wait ran out` +
|
|
1542
|
+
(budget === null ? '' : ` of the suite's ${Math.round(budget / 1000)} s per-test budget`));
|
|
1543
|
+
}
|
|
1544
|
+
if (pendingAssets.length > 0 && room > 0) {
|
|
1545
|
+
const shown = pendingAssets.slice(0, room);
|
|
1546
|
+
room -= shown.length;
|
|
1547
|
+
parts.push(`had no recorded response when the test ended; that alone does not establish the cause: ` +
|
|
1548
|
+
shown.map(requestLabel).join('; '));
|
|
1283
1549
|
}
|
|
1284
1550
|
if (unanswered.length > 0 && room > 0) {
|
|
1285
1551
|
const shown = unanswered.slice(0, room);
|
|
1286
1552
|
room -= shown.length;
|
|
1287
|
-
parts.push(`no response recorded — still in flight when the test ended, so not a server failure
|
|
1288
|
-
shown.map(requestLabel).join('; '));
|
|
1553
|
+
parts.push(`no response recorded — still in flight when the test ended, so not a server failure; ` +
|
|
1554
|
+
`that alone does not establish the cause: ${shown.map(requestLabel).join('; ')}`);
|
|
1289
1555
|
}
|
|
1290
1556
|
if (answered.length > 0 && room > 0) {
|
|
1291
1557
|
// An action that sent nothing leaves the page's own load as the only requests there are:
|
|
1292
1558
|
// name them for what they are, not as the action's answer.
|
|
1293
|
-
const label = step !== undefined && !stalledNav &&
|
|
1559
|
+
const label = step !== undefined && !stalledNav && stepRequests.length === 0
|
|
1294
1560
|
? 'also answered (started before the action)'
|
|
1295
1561
|
: 'answered';
|
|
1296
1562
|
parts.push(`${label}: ${answered.slice(0, room).map(requestLine).join('; ')}`);
|
|
@@ -1326,22 +1592,30 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1326
1592
|
'`MOL_E2E_PREVIEW_PLUGIN=0 npx vite preview --host 127.0.0.1 --port 4173 --strictPort`. '
|
|
1327
1593
|
: '') +
|
|
1328
1594
|
'Fix the server before the app.\n\n';
|
|
1329
|
-
// A navigation
|
|
1595
|
+
// A navigation the RUNNER reported failing, with a subresource that never got a response, and no
|
|
1330
1596
|
// transport error: the page was waiting on the asset when the budget ran out. That is the host
|
|
1331
1597
|
// or the network serving that file, not the app's state — say it before any app-side reading
|
|
1332
1598
|
// (xr-molecule-flash-m135 +34:10: three deployed goto timeouts on tests passing locally in
|
|
1333
1599
|
// 1.2-5.6 s, one 88 KB stylesheet fetch measured at 27 s, and the note said "the cause is in
|
|
1334
|
-
// the page").
|
|
1600
|
+
// the page"). Every traced test must meet the bar; one stalled trace beside a selector-wait
|
|
1601
|
+
// failure is not the run's cause (m153).
|
|
1335
1602
|
const stalledLead = transport !== null || stalledAssets.length === 0
|
|
1336
1603
|
? ''
|
|
1337
|
-
: `Every failure this run traced is a
|
|
1338
|
-
`
|
|
1604
|
+
: `Every failure this run traced is a navigation the runner reported failing ` +
|
|
1605
|
+
`(${stalledWaits.join(', ')}), and the document's own ` +
|
|
1606
|
+
`${stalledAssets.length === 1 ? 'subresource' : 'subresources'} ${stalledAssets.join(', ')} ` +
|
|
1339
1607
|
`never got a response — the navigation was waiting on ${stalledAssets.length === 1 ? 'it' : 'them'} ` +
|
|
1340
1608
|
`when the test's budget ran out. That is the host or the network serving the asset, not the ` +
|
|
1341
|
-
`app's state or a selector:
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1609
|
+
`app's state or a selector: ` +
|
|
1610
|
+
(report.productionBuildAdvised
|
|
1611
|
+
? `the run's own test note already names the next measurement — the same suite against ` +
|
|
1612
|
+
`a production build, because the dev server's page load is not the built app's — take ` +
|
|
1613
|
+
`that one step first, and come back to the app only if the same navigation times out ` +
|
|
1614
|
+
`there too with a stalled asset in the trace.\n\n`
|
|
1615
|
+
: `re-run the same tests once, unchanged, against ${target} before editing anything, ` +
|
|
1616
|
+
`and if the stall repeats measure that exact fetch ` +
|
|
1617
|
+
`(\`curl -o /dev/null -w '%{time_starttransfer} %{time_total}' <url>\`) before ` +
|
|
1618
|
+
`treating it as the app's.\n\n`);
|
|
1345
1619
|
return (transportLead +
|
|
1346
1620
|
stalledLead +
|
|
1347
1621
|
`The platform read the failing tests' own Playwright traces from the run against ${target} ` +
|
|
@@ -1352,14 +1626,26 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1352
1626
|
(anyFailed
|
|
1353
1627
|
? everyFailureLacksPrefix
|
|
1354
1628
|
? ''
|
|
1355
|
-
:
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1629
|
+
: everyFailureAuth
|
|
1630
|
+
? // Every failure was a credential refusal: the handler is not the problem, and the
|
|
1631
|
+
// auth advice already names what is (m148 was sent to the handler six walks running).
|
|
1632
|
+
authAdvice
|
|
1633
|
+
: 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
|
|
1634
|
+
'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
|
|
1635
|
+
'user action silently — log it.' +
|
|
1636
|
+
authAdvice
|
|
1637
|
+
: anyStalled
|
|
1638
|
+
? report.productionBuildAdvised
|
|
1639
|
+
? 'No listed request failed — none got a 4xx/5xx or a network error — but a navigation ' +
|
|
1640
|
+
'the runner reported failing was still waiting on a document subresource that never ' +
|
|
1641
|
+
'answered. Do not read this as a page-state bug, and do not re-run the same tests ' +
|
|
1642
|
+
'unchanged: the run’s own test note names the production build as the next ' +
|
|
1643
|
+
'measurement — take that one step first, and change the app only if the same ' +
|
|
1644
|
+
'navigation times out there with no stalled asset in the trace.'
|
|
1645
|
+
: 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
|
|
1646
|
+
'not finish loading because its own subresource never answered. Do not read this as a ' +
|
|
1647
|
+
'page-state bug: re-run these tests against the same URL first, and change the app only ' +
|
|
1648
|
+
'if the same navigation times out again with no stalled asset in the trace.'
|
|
1363
1649
|
: 'No listed request failed — none got a 4xx/5xx or a network error — so the cause is in the ' +
|
|
1364
1650
|
"page, not the API. Start with what the test's last action sent (the first line of each " +
|
|
1365
1651
|
'row) and what the page did next: a request that answered (2xx included) while the page ' +
|