@molecule/api-ai-tools 1.9.9 → 1.9.11
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 +65 -12
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +62 -2
- package/dist/trace-artifacts.d.ts +71 -15
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +336 -92
- 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:24:21.171Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -397,6 +397,44 @@ interface TraceRequest {
|
|
|
397
397
|
* `net::ERR_*`, nothing rendered and no selector or app request was measured.
|
|
398
398
|
*/
|
|
399
399
|
type?: string
|
|
400
|
+
/**
|
|
401
|
+
* When the request started, on the trace's own monotonic clock (ms) — the same clock the
|
|
402
|
+
* {@link TraceAction}s carry, so a request can be attributed to the action that caused it.
|
|
403
|
+
* Absent on traces whose network entries recorded no clock (an older Playwright, a synthetic
|
|
404
|
+
* fixture), in which case only recording order is available.
|
|
405
|
+
*/
|
|
406
|
+
startedAt?: number
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### `TraceLastAction`
|
|
411
|
+
|
|
412
|
+
The test's LAST user action before the failing assertion, and the requests it caused.
|
|
413
|
+
|
|
414
|
+
An assertion after a navigation is not explained by the page's last call (`GET /api/tags` on
|
|
415
|
+
whatever page the app redirected to); it is explained by what the action the test took sent and
|
|
416
|
+
what came back (xr-molecule-flash-m148: 13 then 12 failing tests, every trace row leading with
|
|
417
|
+
`GET /api/tags → 200`, while the register `POST /api/users` and its answer — the fact that
|
|
418
|
+
explains the signed-out page — was never shown).
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
interface TraceLastAction {
|
|
422
|
+
/** The Playwright API the trace names: `Locator.click`, `Locator.fill`, `Frame.goto`. */
|
|
423
|
+
api: string
|
|
424
|
+
/** When the action started, on the trace's monotonic clock (ms). */
|
|
425
|
+
start: number
|
|
426
|
+
/** When it finished, or null when the run ended before it did. */
|
|
427
|
+
end: number | null
|
|
428
|
+
/**
|
|
429
|
+
* The requests the action caused — started after it and before the failing assertion, the app's
|
|
430
|
+
* own API calls first, static assets and the page shell's own files left out. Empty when the
|
|
431
|
+
* action sent none.
|
|
432
|
+
*/
|
|
433
|
+
requests: TraceRequest[]
|
|
434
|
+
/** The main frame's URL when the action ran, when the trace recorded frame snapshots. */
|
|
435
|
+
pageBefore?: string
|
|
436
|
+
/** The last main-frame URL the trace recorded — where the page ended up. */
|
|
437
|
+
pageAfter?: string
|
|
400
438
|
}
|
|
401
439
|
```
|
|
402
440
|
|
|
@@ -417,7 +455,7 @@ interface TraceAction {
|
|
|
417
455
|
|
|
418
456
|
#### `TraceArtifact`
|
|
419
457
|
|
|
420
|
-
What one trace says: the test it belongs to, the requests it made, and its console
|
|
458
|
+
What one trace says: the test it belongs to, the requests it made, and its console lines.
|
|
421
459
|
|
|
422
460
|
```typescript
|
|
423
461
|
interface TraceArtifact {
|
|
@@ -428,17 +466,31 @@ interface TraceArtifact {
|
|
|
428
466
|
/** The requests the page made, the interesting ones first ({@link traceArtifactsScript}). */
|
|
429
467
|
requests: TraceRequest[]
|
|
430
468
|
/**
|
|
431
|
-
* The request the failing step itself sent: the
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
469
|
+
* The request the failing step itself sent: the first answered request the test's last user
|
|
470
|
+
* action caused ({@link TraceArtifact.lastAction}), else — on a trace with no action clock — the
|
|
471
|
+
* trace's LAST `fetch`/`xhr` in recording order, else its document. It leads the note's row when
|
|
472
|
+
* {@link lastAction} is absent, because a click that changed nothing is explained by the request
|
|
473
|
+
* the click sent and what came back — xr-molecule-flash-m135's Follow click sent
|
|
474
|
+
* `DELETE /api/profiles/x/follow → 200` (its `following` state was stale) and the note listed only
|
|
475
|
+
* the test's earlier, answered requests.
|
|
436
476
|
*
|
|
437
477
|
* Absent when the trace recorded neither, and on traces read before this field existed.
|
|
438
478
|
*/
|
|
439
479
|
lead?: TraceRequest
|
|
440
|
-
/**
|
|
480
|
+
/**
|
|
481
|
+
* The console lines and page errors the page raised: its errors first, then a line that names a
|
|
482
|
+
* session decision or comes from a molecule package, then any other warning, then the browser's
|
|
483
|
+
* own resource error ({@link traceArtifactsScript}).
|
|
484
|
+
*/
|
|
441
485
|
console: string[]
|
|
486
|
+
/**
|
|
487
|
+
* The test's LAST user action before the failing assertion and the requests it caused — what the
|
|
488
|
+
* row leads with, when the trace carries the action times ({@link TraceLastAction}).
|
|
489
|
+
*
|
|
490
|
+
* Absent on traces read before this field existed, and on traces whose network entries recorded
|
|
491
|
+
* no clock; the row then falls back to {@link lead}.
|
|
492
|
+
*/
|
|
493
|
+
lastAction?: TraceLastAction
|
|
442
494
|
/**
|
|
443
495
|
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
|
|
444
496
|
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
|
|
@@ -1771,7 +1823,7 @@ function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool
|
|
|
1771
1823
|
|
|
1772
1824
|
The Node program the sandbox runs to read a failed run's traces: `node <script> <dir>` prints one
|
|
1773
1825
|
`TRACE <json>` line per `trace.zip` under `<dir>` (newest first, {@link MAX_TRACE_ARTIFACTS} of
|
|
1774
|
-
them), each with the test it belongs to, the requests its page made and its console
|
|
1826
|
+
them), each with the test it belongs to, the requests its page made and its console lines.
|
|
1775
1827
|
|
|
1776
1828
|
It is a standalone program, not an import, for the reason `@molecule/api-code-sandbox` runs it:
|
|
1777
1829
|
Playwright's own zip reader is not reachable from a project's `node_modules`, and a dependency
|
|
@@ -1900,8 +1952,8 @@ A 4xx request carries what the page SENT beside its status, so a wrapped body is
|
|
|
1900
1952
|
line rather than inferred from the status; when one of those bodies is an object whose only key
|
|
1901
1953
|
is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
|
|
1902
1954
|
|
|
1903
|
-
Every row opens with the
|
|
1904
|
-
|
|
1955
|
+
Every row opens with the test's LAST user action before the failing assertion and the requests
|
|
1956
|
+
that action caused ({@link TraceArtifact.lastAction}) — started after it, before the assertion,
|
|
1905
1957
|
|
|
1906
1958
|
_(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts`.)_
|
|
1907
1959
|
|
|
@@ -3173,7 +3225,8 @@ const MAX_TRACED_REQUESTS: 6
|
|
|
3173
3225
|
|
|
3174
3226
|
#### `MAX_TRACED_CONSOLE`
|
|
3175
3227
|
|
|
3176
|
-
How many console
|
|
3228
|
+
How many console lines one test's trace contributes: errors ahead of a session or molecule-package
|
|
3229
|
+
line, both ahead of a plain warning and of the browser's own resource error.
|
|
3177
3230
|
|
|
3178
3231
|
```typescript
|
|
3179
3232
|
const MAX_TRACED_CONSOLE: 4
|
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;
|
|
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,CAu/ExF"}
|
package/dist/tools.js
CHANGED
|
@@ -363,7 +363,7 @@ async function backgroundTaskState(backend, command, seconds, ceilingMs) {
|
|
|
363
363
|
}
|
|
364
364
|
import { suiteCommandTarget } from './suite-overlap.js';
|
|
365
365
|
import { diagnosisTraceCommand } from './trace-artifacts.js';
|
|
366
|
-
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, earlyReturnCause, emptyCallError, failureCausesNote, findPathArgs, findPatternArgs, fusedLine, htmlOutline, isCommandTimeout, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isSuiteRunCommand, isValidGlob, linesWithinSerialized, literalDirPrefix, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, searchTimeoutError, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPipelinePlacement, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, withoutSuiteTimeout, } from './utilities.js';
|
|
366
|
+
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, earlyReturnCause, emptyCallError, failureCausesNote, findPathArgs, findPatternArgs, fusedLine, htmlOutline, isCommandTimeout, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isSuiteRunCommand, isValidGlob, linesWithinSerialized, literalDirPrefix, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, searchTimeoutError, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPipelinePlacement, suiteRunPriorityLaunch, TOOL_RESULT_CHARS, truncateMiddle, universalCause, whitespaceTolerantReplace, withoutSuiteTimeout, } from './utilities.js';
|
|
367
367
|
/**
|
|
368
368
|
* How long exec_command waits for a command it backgrounded only because the
|
|
369
369
|
* command asked for more than the tool's ceiling, before handing back a task id.
|
|
@@ -610,6 +610,33 @@ function sandboxStillStarting(error) {
|
|
|
610
610
|
return true;
|
|
611
611
|
return /not (?:found|running)|notfound|not attached|still starting/i.test(`${name} ${message}`);
|
|
612
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* The most entries a bounded `list_files` result lists. A package catalog is not read entry by
|
|
615
|
+
* entry — the executor wants one package it can name — so the summary keeps enough names to show
|
|
616
|
+
* the naming pattern and no more.
|
|
617
|
+
*/
|
|
618
|
+
const LIST_FILES_HEAD_ENTRIES = 20;
|
|
619
|
+
/**
|
|
620
|
+
* The line a package-catalog listing carries instead of a catalog's worth of names: a directory
|
|
621
|
+
* of 1,032 names answers no question, while `find_package` answers the one that was asked.
|
|
622
|
+
*/
|
|
623
|
+
const MOLECULE_CATALOG_POINTER = 'use find_package(query) to search the @molecule catalog by what you need, and ' +
|
|
624
|
+
'read_molecule_doc(name) for one package';
|
|
625
|
+
/**
|
|
626
|
+
* Which package catalog a `list_files` path is, when it is one: the `@molecule` scope (1,032
|
|
627
|
+
* package directories, ~53 KB of names in one plan-phase call — X0 xr-molecule-flash-m151 at
|
|
628
|
+
* +01:54) or the `node_modules` root above it.
|
|
629
|
+
*
|
|
630
|
+
* @param path - A resolved (absolute, normalized) directory path.
|
|
631
|
+
* @returns `'molecule'` for the `@molecule` scope, `'packages'` for a `node_modules` root, or null.
|
|
632
|
+
*/
|
|
633
|
+
function packageCatalog(path) {
|
|
634
|
+
if (/(^|\/)node_modules\/@molecule$/.test(path))
|
|
635
|
+
return 'molecule';
|
|
636
|
+
if (/(^|\/)node_modules$/.test(path))
|
|
637
|
+
return 'packages';
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
613
640
|
/**
|
|
614
641
|
* Build a complete set of AI agent tools bound to an execution backend.
|
|
615
642
|
*
|
|
@@ -1161,6 +1188,39 @@ export function buildTools(backend, config) {
|
|
|
1161
1188
|
return { error: directoryReadHint(message, path) ?? `Failed to read ${path}: ${message}` };
|
|
1162
1189
|
}
|
|
1163
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* A `list_files` result, bounded when the directory is a package catalog or when the whole
|
|
1193
|
+
* listing would not fit one tool result ({@link TOOL_RESULT_CHARS} — molecule-dev's cap on a
|
|
1194
|
+
* tool result's text, which cuts a longer one through its MIDDLE). A normal app directory whose
|
|
1195
|
+
* listing fits comes back exactly as it always has.
|
|
1196
|
+
*
|
|
1197
|
+
* @param path - The resolved directory path.
|
|
1198
|
+
* @param entries - Everything the backend listed.
|
|
1199
|
+
* @returns The listing, or its count plus the first {@link LIST_FILES_HEAD_ENTRIES} names and a note.
|
|
1200
|
+
*/
|
|
1201
|
+
function listFilesResult(path, entries) {
|
|
1202
|
+
const listing = {
|
|
1203
|
+
path,
|
|
1204
|
+
entries: entries.map((entry) => ({ name: entry.name, type: entry.type })),
|
|
1205
|
+
};
|
|
1206
|
+
const catalog = packageCatalog(path);
|
|
1207
|
+
if (!catalog && JSON.stringify(listing).length <= TOOL_RESULT_CHARS)
|
|
1208
|
+
return listing;
|
|
1209
|
+
const head = listing.entries.slice(0, LIST_FILES_HEAD_ENTRIES);
|
|
1210
|
+
const cut = head.length < entries.length;
|
|
1211
|
+
const note = catalog === 'molecule'
|
|
1212
|
+
? `${entries.length} @molecule packages are installed here` +
|
|
1213
|
+
(cut ? `; this listing is cut to ${head.length} names` : '') +
|
|
1214
|
+
`. ${MOLECULE_CATALOG_POINTER}.`
|
|
1215
|
+
: catalog === 'packages'
|
|
1216
|
+
? `This node_modules holds ${entries.length} entries` +
|
|
1217
|
+
(cut ? `; this listing is cut to ${head.length} names` : '') +
|
|
1218
|
+
`. ${MOLECULE_CATALOG_POINTER}.`
|
|
1219
|
+
: `This directory holds ${entries.length} entries` +
|
|
1220
|
+
(cut ? `; this listing is cut to ${head.length}` : '') +
|
|
1221
|
+
'. Narrow it with find_files (by path pattern) or search_files (by content), or list a subdirectory.';
|
|
1222
|
+
return { path, count: entries.length, entries: head, note };
|
|
1223
|
+
}
|
|
1164
1224
|
const toolImpls = {
|
|
1165
1225
|
async list_files(input) {
|
|
1166
1226
|
const path = resolve(input.path || '');
|
|
@@ -1169,7 +1229,7 @@ export function buildTools(backend, config) {
|
|
|
1169
1229
|
return { error: symlinkErr };
|
|
1170
1230
|
try {
|
|
1171
1231
|
const entries = await backend.readDir(path);
|
|
1172
|
-
return
|
|
1232
|
+
return listFilesResult(path, entries);
|
|
1173
1233
|
}
|
|
1174
1234
|
catch (e) {
|
|
1175
1235
|
const message = e.message;
|
|
@@ -60,6 +60,40 @@ export interface TraceRequest {
|
|
|
60
60
|
* `net::ERR_*`, nothing rendered and no selector or app request was measured.
|
|
61
61
|
*/
|
|
62
62
|
type?: string;
|
|
63
|
+
/**
|
|
64
|
+
* When the request started, on the trace's own monotonic clock (ms) — the same clock the
|
|
65
|
+
* {@link TraceAction}s carry, so a request can be attributed to the action that caused it.
|
|
66
|
+
* Absent on traces whose network entries recorded no clock (an older Playwright, a synthetic
|
|
67
|
+
* fixture), in which case only recording order is available.
|
|
68
|
+
*/
|
|
69
|
+
startedAt?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The test's LAST user action before the failing assertion, and the requests it caused.
|
|
73
|
+
*
|
|
74
|
+
* An assertion after a navigation is not explained by the page's last call (`GET /api/tags` on
|
|
75
|
+
* whatever page the app redirected to); it is explained by what the action the test took sent and
|
|
76
|
+
* what came back (xr-molecule-flash-m148: 13 then 12 failing tests, every trace row leading with
|
|
77
|
+
* `GET /api/tags → 200`, while the register `POST /api/users` and its answer — the fact that
|
|
78
|
+
* explains the signed-out page — was never shown).
|
|
79
|
+
*/
|
|
80
|
+
export interface TraceLastAction {
|
|
81
|
+
/** The Playwright API the trace names: `Locator.click`, `Locator.fill`, `Frame.goto`. */
|
|
82
|
+
api: string;
|
|
83
|
+
/** When the action started, on the trace's monotonic clock (ms). */
|
|
84
|
+
start: number;
|
|
85
|
+
/** When it finished, or null when the run ended before it did. */
|
|
86
|
+
end: number | null;
|
|
87
|
+
/**
|
|
88
|
+
* The requests the action caused — started after it and before the failing assertion, the app's
|
|
89
|
+
* own API calls first, static assets and the page shell's own files left out. Empty when the
|
|
90
|
+
* action sent none.
|
|
91
|
+
*/
|
|
92
|
+
requests: TraceRequest[];
|
|
93
|
+
/** The main frame's URL when the action ran, when the trace recorded frame snapshots. */
|
|
94
|
+
pageBefore?: string;
|
|
95
|
+
/** The last main-frame URL the trace recorded — where the page ended up. */
|
|
96
|
+
pageAfter?: string;
|
|
63
97
|
}
|
|
64
98
|
/** One action the traced page performed, with the trace's own timing for it. */
|
|
65
99
|
export interface TraceAction {
|
|
@@ -70,7 +104,7 @@ export interface TraceAction {
|
|
|
70
104
|
/** When it finished, or null when the run ended before it did. */
|
|
71
105
|
end: number | null;
|
|
72
106
|
}
|
|
73
|
-
/** What one trace says: the test it belongs to, the requests it made, and its console
|
|
107
|
+
/** What one trace says: the test it belongs to, the requests it made, and its console lines. */
|
|
74
108
|
export interface TraceArtifact {
|
|
75
109
|
/** The failure's spec file, relative to the run's directory, when the trace names one. */
|
|
76
110
|
spec: string | null;
|
|
@@ -79,17 +113,31 @@ export interface TraceArtifact {
|
|
|
79
113
|
/** The requests the page made, the interesting ones first ({@link traceArtifactsScript}). */
|
|
80
114
|
requests: TraceRequest[];
|
|
81
115
|
/**
|
|
82
|
-
* The request the failing step itself sent: the
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
116
|
+
* The request the failing step itself sent: the first answered request the test's last user
|
|
117
|
+
* action caused ({@link TraceArtifact.lastAction}), else — on a trace with no action clock — the
|
|
118
|
+
* trace's LAST `fetch`/`xhr` in recording order, else its document. It leads the note's row when
|
|
119
|
+
* {@link lastAction} is absent, because a click that changed nothing is explained by the request
|
|
120
|
+
* the click sent and what came back — xr-molecule-flash-m135's Follow click sent
|
|
121
|
+
* `DELETE /api/profiles/x/follow → 200` (its `following` state was stale) and the note listed only
|
|
122
|
+
* the test's earlier, answered requests.
|
|
87
123
|
*
|
|
88
124
|
* Absent when the trace recorded neither, and on traces read before this field existed.
|
|
89
125
|
*/
|
|
90
126
|
lead?: TraceRequest;
|
|
91
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* The console lines and page errors the page raised: its errors first, then a line that names a
|
|
129
|
+
* session decision or comes from a molecule package, then any other warning, then the browser's
|
|
130
|
+
* own resource error ({@link traceArtifactsScript}).
|
|
131
|
+
*/
|
|
92
132
|
console: string[];
|
|
133
|
+
/**
|
|
134
|
+
* The test's LAST user action before the failing assertion and the requests it caused — what the
|
|
135
|
+
* row leads with, when the trace carries the action times ({@link TraceLastAction}).
|
|
136
|
+
*
|
|
137
|
+
* Absent on traces read before this field existed, and on traces whose network entries recorded
|
|
138
|
+
* no clock; the row then falls back to {@link lead}.
|
|
139
|
+
*/
|
|
140
|
+
lastAction?: TraceLastAction;
|
|
93
141
|
/**
|
|
94
142
|
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
|
|
95
143
|
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
|
|
@@ -101,7 +149,10 @@ export interface TraceArtifact {
|
|
|
101
149
|
export declare const MAX_TRACE_ARTIFACTS = 12;
|
|
102
150
|
/** How many requests one test's trace contributes. */
|
|
103
151
|
export declare const MAX_TRACED_REQUESTS = 6;
|
|
104
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* How many console lines one test's trace contributes: errors ahead of a session or molecule-package
|
|
154
|
+
* line, both ahead of a plain warning and of the browser's own resource error.
|
|
155
|
+
*/
|
|
105
156
|
export declare const MAX_TRACED_CONSOLE = 4;
|
|
106
157
|
/** How many of the page's own actions one test's trace contributes, the LAST ones. */
|
|
107
158
|
export declare const MAX_TRACED_ACTIONS = 40;
|
|
@@ -126,7 +177,7 @@ export declare const TRACE_NOTE_LEAD_GOT_CHARS = 200;
|
|
|
126
177
|
/**
|
|
127
178
|
* The Node program the sandbox runs to read a failed run's traces: `node <script> <dir>` prints one
|
|
128
179
|
* `TRACE <json>` line per `trace.zip` under `<dir>` (newest first, {@link MAX_TRACE_ARTIFACTS} of
|
|
129
|
-
* them), each with the test it belongs to, the requests its page made and its console
|
|
180
|
+
* them), each with the test it belongs to, the requests its page made and its console lines.
|
|
130
181
|
*
|
|
131
182
|
* It is a standalone program, not an import, for the reason `@molecule/api-code-sandbox` runs it:
|
|
132
183
|
* Playwright's own zip reader is not reachable from a project's `node_modules`, and a dependency
|
|
@@ -226,14 +277,19 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
226
277
|
* line rather than inferred from the status; when one of those bodies is an object whose only key
|
|
227
278
|
* is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
|
|
228
279
|
*
|
|
229
|
-
* Every row opens with the
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
280
|
+
* Every row opens with the test's LAST user action before the failing assertion and the requests
|
|
281
|
+
* that action caused ({@link TraceArtifact.lastAction}) — started after it, before the assertion,
|
|
282
|
+
* the app's own API calls first, each with its status and the first 200 characters of its response,
|
|
283
|
+
* then where the page went next. The trace's action clock and each request's `_monotonicTime` are
|
|
284
|
+
* the same clock, so "caused" is the trace's own fact rather than a guess from recording order.
|
|
285
|
+
* xr-molecule-flash-m148 failed 13 then 12 tests on one unnamed cause while every row led with
|
|
286
|
+
* `GET /api/tags → 200` — the Home page's own fetch after the redirect — and filled its answered
|
|
287
|
+
* slots with the PWA's `/workbox-*.js` and its `/icons/*.svg`; the register `POST /api/users`
|
|
288
|
+
* (answered 201, the page still rendering signed-out) never appeared. With no action clock, the
|
|
289
|
+
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
234
290
|
*
|
|
235
291
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
236
|
-
* needs a recorded response, a real failure or a console
|
|
292
|
+
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
237
293
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
238
294
|
* earlier walk → the rest ({@link traceRowTier}). Without that the note spent all five rows on four
|
|
239
295
|
* traces of one `afterEach` line whose every request was `-1`, and left the favorite cause — the
|
|
@@ -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;
|
|
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
|
|
@@ -90,10 +93,25 @@ const zlib = require('node:zlib')
|
|
|
90
93
|
const ROOT = process.argv[2] || '/workspace'
|
|
91
94
|
const MAX_TRACES = ${MAX_TRACE_ARTIFACTS}
|
|
92
95
|
const MAX_REQUESTS = ${MAX_TRACED_REQUESTS}
|
|
96
|
+
const MAX_NOTE = ${MAX_TRACE_NOTE_REQUESTS}
|
|
93
97
|
const MAX_CONSOLE = ${MAX_TRACED_CONSOLE}
|
|
94
98
|
const MAX_ACTIONS = ${MAX_TRACED_ACTIONS}
|
|
95
99
|
const MAX_BODY = ${MAX_TRACED_BODY_CHARS}
|
|
96
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
|
+
}
|
|
97
115
|
|
|
98
116
|
function walk(dir, depth, out) {
|
|
99
117
|
if (depth < 0) return
|
|
@@ -231,6 +249,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
231
249
|
status: Number(response.status) || 0,
|
|
232
250
|
failure: typeof response._failureText === 'string' ? response._failureText : undefined,
|
|
233
251
|
type: type,
|
|
252
|
+
// The HAR entry carries the monotonic clock the trace's action entries use, so a request
|
|
253
|
+
// can be attributed to the action that caused it (see \`lastAction\` below).
|
|
254
|
+
startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
|
|
234
255
|
postData: request.postData,
|
|
235
256
|
content: response.content,
|
|
236
257
|
})
|
|
@@ -240,16 +261,6 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
240
261
|
origin = first ? originOf(first.path) : ''
|
|
241
262
|
}
|
|
242
263
|
for (const r of requests) r.path = pathOf(r.path).slice(0, MAX_URL)
|
|
243
|
-
// A 4xx from the app's own API is where a body SHAPE is diagnosed: keep what the page sent and
|
|
244
|
-
// what came back (bounded). A relative path here means the same origin as the traced document.
|
|
245
|
-
for (const r of requests) {
|
|
246
|
-
if (r.status < 400 || r.status >= 500) continue
|
|
247
|
-
if (/^https?:/i.test(r.path)) continue
|
|
248
|
-
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
249
|
-
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
250
|
-
if (sent) r.sent = sent
|
|
251
|
-
if (got) r.got = got
|
|
252
|
-
}
|
|
253
264
|
// The page's own actions, timed. before/after pair on the call id, and only the LIBRARY
|
|
254
265
|
// traces carry a class and a method (Frame.goto, Locator.fill); the test runner's own
|
|
255
266
|
// trace times its hooks and fixtures with class Test, which is not a page action. Read BEFORE
|
|
@@ -283,20 +294,36 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
283
294
|
// model to write a caching service worker into the app). A document's OWN stylesheet or
|
|
284
295
|
// script is not shell noise: the navigation blocks on it.
|
|
285
296
|
const SHELL_TYPE = { font: 1, image: 1, stylesheet: 1, script: 1, manifest: 1 }
|
|
297
|
+
// The PWA worker's own hashed script (\`/workbox-d8ed3718.js\`), the manifest's touch icons
|
|
298
|
+
// (\`/icons/splash-*.svg\`), the favicons and fonts: the shell's files. They are typed \`other\` or
|
|
299
|
+
// \`fetch\` by the worker and the shell rather than \`script\`/\`image\`, so the type alone never
|
|
300
|
+
// caught them — on xr-molecule-flash-m148 they took three of the note's four slots on every
|
|
301
|
+
// failing test and pushed the register \`POST /api/users\` out.
|
|
286
302
|
function shellPath(path) {
|
|
287
|
-
|
|
288
|
-
|
|
303
|
+
var p = path
|
|
304
|
+
try { p = new URL(path, 'http://trace.local').pathname } catch (e) { p = String(path).split(/[?#]/)[0] }
|
|
305
|
+
return /^\\/(?:sw|service-worker)\\.js(?:[?#]|$)|\\/manifest\\.webmanifest(?:[?#]|$)|\\/favicon[^/]*(?:[?#]|$)|\\/fonts?\\//.test(p) ||
|
|
306
|
+
/^\\/(?:assets|@vite|__mol)\\//.test(p) ||
|
|
307
|
+
/^\\/workbox-[^/]*\\.js(?:[?#]|$)/.test(p) ||
|
|
308
|
+
/^\\/icons\\//.test(p) ||
|
|
309
|
+
/^\\/(?:apple-touch-icon|og-image|splash)[^/]*(?:[?#]|$)/.test(p)
|
|
289
310
|
}
|
|
290
311
|
function shellRequest(r) {
|
|
291
312
|
if (r.type === 'document') return false
|
|
292
313
|
return SHELL_TYPE[r.type] === 1 || shellPath(r.path)
|
|
293
314
|
}
|
|
294
315
|
// The shell requests that stay noise even when they never finished: the PWA worker, the
|
|
295
|
-
// manifest, a favicon, a font
|
|
316
|
+
// manifest, a favicon, a font, the worker's own script and the manifest's icons. A pending
|
|
317
|
+
// \`/assets/*.css\` is NOT one of these — the navigation blocks on it (m135).
|
|
296
318
|
function incidentalShell(r) {
|
|
297
319
|
if (r.type === 'document') return false
|
|
298
|
-
|
|
299
|
-
|
|
320
|
+
if (r.type === 'font' || r.type === 'manifest') return true
|
|
321
|
+
var p = r.path
|
|
322
|
+
try { p = new URL(r.path, 'http://trace.local').pathname } catch (e) { p = String(r.path).split(/[?#]/)[0] }
|
|
323
|
+
return /^\\/(?:sw|service-worker)\\.js(?:[?#]|$)|\\/manifest\\.webmanifest(?:[?#]|$)|\\/favicon[^/]*(?:[?#]|$)|\\/fonts?\\//.test(p) ||
|
|
324
|
+
/^\\/workbox-[^/]*\\.js(?:[?#]|$)/.test(p) ||
|
|
325
|
+
/^\\/icons\\//.test(p) ||
|
|
326
|
+
/^\\/(?:apple-touch-icon|og-image|splash)[^/]*(?:[?#]|$)/.test(p)
|
|
300
327
|
}
|
|
301
328
|
// A failed request: a 4xx/5xx, or no response at all WITH Playwright's error text (a refused
|
|
302
329
|
// connection). No response and no text is an aborted/pending request, not a failure. A 404 on
|
|
@@ -309,15 +336,19 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
309
336
|
function neverFinished(r) {
|
|
310
337
|
return r.status <= 0 && typeof r.failure !== 'string'
|
|
311
338
|
}
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
339
|
+
// Whether a request can lead a row: it got an answer (or failed for its own recorded reason),
|
|
340
|
+
// and it is the app's own call — a fetch/xhr of the page, or its document — never the page
|
|
341
|
+
// shell's own probe (a refused \`/sw.js\` is not the step's request — m125 +29:33).
|
|
342
|
+
//
|
|
343
|
+
// It ranks the requests the test's last action caused (see \`lastAction\` below), and it is the
|
|
344
|
+
// whole rule on a trace with no action clock (the fallback lead: the page's LAST call to the
|
|
345
|
+
// app's own origin, else the document load). That fallback is right for a click that changed
|
|
346
|
+
// nothing — xr-molecule-flash-m135 clicked Follow and sent a DELETE (its \`following\` state was
|
|
347
|
+
// stale) — and wrong for an assertion after a navigation (m148), which is why the action's
|
|
348
|
+
// clock, when the trace has one, decides.
|
|
317
349
|
//
|
|
318
350
|
// Never a request that is still in flight (no response, no error text): it says nothing about
|
|
319
|
-
// what came back, and the note has its own honest row for it (m112 +30:45).
|
|
320
|
-
// shell's own probe (a refused \`/sw.js\` is not the step's request — m125 +29:33).
|
|
351
|
+
// what came back, and the note has its own honest row for it (m112 +30:45).
|
|
321
352
|
function leadCandidate(r) {
|
|
322
353
|
if (r.status <= 0 && typeof r.failure !== 'string') return false
|
|
323
354
|
if (r.type === 'document') return true
|
|
@@ -329,29 +360,131 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
329
360
|
try { p = new URL(r.path, 'http://trace.local').pathname } catch (e) { p = String(r.path).split(/[?#]/)[0] }
|
|
330
361
|
return !shellPath(p)
|
|
331
362
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
363
|
+
// The test's LAST user action before the failing assertion, and the requests it caused.
|
|
364
|
+
//
|
|
365
|
+
// The old lead was "the page's LAST call to the app's own origin", which is right for a click
|
|
366
|
+
// that changed nothing (m135) and wrong for an assertion after a navigation: the last call
|
|
367
|
+
// belongs to the page the app landed on (xr-molecule-flash-m148's \`GET /api/tags\` on Home),
|
|
368
|
+
// not to the action the test took. The trace records both clocks — the action's \`startTime\`
|
|
369
|
+
// and each HAR entry's \`_monotonicTime\` — so the requests that started after the last action
|
|
370
|
+
// are exactly the ones it caused, and the register \`POST /api/users\` (answered 201, the page
|
|
371
|
+
// then rendering signed-out) leads instead of the Home page's own fetch.
|
|
372
|
+
const USER_ACTION = /\\.(?:click|dblclick|tap|fill|press|pressSequentially|selectOption|check|uncheck|setInputFiles|goto|type)$/
|
|
373
|
+
const userActions = actions.filter(function (a) { return USER_ACTION.test(a.api) })
|
|
374
|
+
const lastAction = userActions.length > 0 ? userActions[userActions.length - 1] : null
|
|
375
|
+
// A request the action caused started after it (a millisecond of clock slack) and before the
|
|
376
|
+
// assertion — which is everything later in the trace. A request that started before it is the
|
|
377
|
+
// page's own load, not the action's; the page shell's files are never the step's, except a
|
|
378
|
+
// document subresource the open navigation is waiting on (m135).
|
|
379
|
+
function causedIndexes(action) {
|
|
380
|
+
const out = []
|
|
381
|
+
for (let i = 0; i < requests.length; i++) {
|
|
382
|
+
const r = requests[i]
|
|
383
|
+
if (typeof r.startedAt !== 'number' || r.startedAt < action.start - 1) continue
|
|
384
|
+
if (shellRequest(r) && !(navigationOpen && neverFinished(r) && !incidentalShell(r))) continue
|
|
385
|
+
out.push(i)
|
|
386
|
+
}
|
|
387
|
+
return out
|
|
336
388
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
389
|
+
// API requests first, then the navigation the page made; a stalled asset outranks both when the
|
|
390
|
+
// navigation it belongs to never finished (m135's goto timeout was waiting on the CSS).
|
|
391
|
+
function causedOrder(indexes) {
|
|
392
|
+
const stalled = []
|
|
393
|
+
const apiCalls = []
|
|
394
|
+
const documents = []
|
|
395
|
+
const other = []
|
|
396
|
+
for (const i of indexes) {
|
|
397
|
+
const r = requests[i]
|
|
398
|
+
if (r.type === 'document') documents.push(i)
|
|
399
|
+
else if (navigationOpen && neverFinished(r) && !incidentalShell(r)) stalled.push(i)
|
|
400
|
+
else if (r.type === 'fetch' || r.type === 'xhr') apiCalls.push(i)
|
|
401
|
+
else other.push(i)
|
|
340
402
|
}
|
|
403
|
+
return stalled.concat(apiCalls, documents, other)
|
|
341
404
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
405
|
+
const actionIndexes = lastAction === null ? [] : causedOrder(causedIndexes(lastAction))
|
|
406
|
+
// Both the action's own requests and a 4xx from the app's own API keep what the page sent and
|
|
407
|
+
// what came back. The action's requests keep the RESPONSE whatever its status — an answered
|
|
408
|
+
// \`POST /api/users → 201\` and its body is the fact that explains a signed-out page.
|
|
409
|
+
for (const i of actionIndexes) {
|
|
410
|
+
const r = requests[i]
|
|
411
|
+
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
412
|
+
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
413
|
+
if (sent) r.sent = sent
|
|
414
|
+
if (got) r.got = got
|
|
415
|
+
}
|
|
416
|
+
for (const r of requests) {
|
|
417
|
+
if (r.status < 400 || r.status >= 500) continue
|
|
418
|
+
if (/^https?:/i.test(r.path)) continue
|
|
345
419
|
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
346
420
|
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
347
|
-
|
|
421
|
+
if (sent) r.sent = sent
|
|
422
|
+
if (got) r.got = got
|
|
423
|
+
}
|
|
424
|
+
function wire(r) {
|
|
425
|
+
return {
|
|
348
426
|
method: r.method,
|
|
349
427
|
path: r.path,
|
|
350
428
|
status: r.status,
|
|
351
429
|
failure: r.failure,
|
|
430
|
+
...(r.sent ? { sent: r.sent } : {}),
|
|
431
|
+
...(r.got ? { got: r.got } : {}),
|
|
352
432
|
type: r.type || undefined,
|
|
353
|
-
|
|
354
|
-
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
|
|
436
|
+
// The trace records these around every action, which is how the note can say what the page did
|
|
437
|
+
// after the request — a client-side redirect never appears in the network log as a document.
|
|
438
|
+
const frames = []
|
|
439
|
+
for (const entry of trace) {
|
|
440
|
+
if (!entry || entry.type !== 'frame-snapshot') continue
|
|
441
|
+
const snap = entry.snapshot || {}
|
|
442
|
+
if (snap.isMainFrame === false) continue
|
|
443
|
+
if (typeof snap.frameUrl !== 'string' || typeof snap.timestamp !== 'number') continue
|
|
444
|
+
frames.push({ url: pathOf(snap.frameUrl).slice(0, MAX_URL), at: snap.timestamp })
|
|
445
|
+
}
|
|
446
|
+
let last = null
|
|
447
|
+
if (lastAction !== null) {
|
|
448
|
+
let pageBefore
|
|
449
|
+
for (const frame of frames) { if (frame.at <= lastAction.start) pageBefore = frame.url }
|
|
450
|
+
if (pageBefore === undefined && frames.length > 0) pageBefore = frames[0].url
|
|
451
|
+
const pageAfter = frames.length > 0 ? frames[frames.length - 1].url : undefined
|
|
452
|
+
last = {
|
|
453
|
+
api: lastAction.api,
|
|
454
|
+
start: lastAction.start,
|
|
455
|
+
end: lastAction.end,
|
|
456
|
+
requests: actionIndexes.slice(0, MAX_NOTE).map(function (i) { return wire(requests[i]) }),
|
|
457
|
+
...(pageBefore === undefined ? {} : { pageBefore: pageBefore }),
|
|
458
|
+
...(pageAfter === undefined ? {} : { pageAfter: pageAfter }),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// The lead stays for traces that carry no action clock: the last answered fetch/xhr, else the
|
|
462
|
+
// document. With an action, its first answered request is the lead.
|
|
463
|
+
let lead = null
|
|
464
|
+
for (const i of actionIndexes) {
|
|
465
|
+
if (leadCandidate(requests[i])) { lead = wire(requests[i]); break }
|
|
466
|
+
}
|
|
467
|
+
// The fallback lead stays for traces with no action clock. With an action, only a request it
|
|
468
|
+
// caused is the lead; an action that sent none leaves the page's own load to the row's
|
|
469
|
+
// "also answered" bucket, not to a lead that would hide it.
|
|
470
|
+
if (lead === null && lastAction === null) {
|
|
471
|
+
let leadIndex = -1
|
|
472
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
473
|
+
const r = requests[i]
|
|
474
|
+
if ((r.type === 'fetch' || r.type === 'xhr') && leadCandidate(r)) { leadIndex = i; break }
|
|
475
|
+
}
|
|
476
|
+
if (leadIndex === -1) {
|
|
477
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
478
|
+
if (requests[i].type === 'document' && leadCandidate(requests[i])) { leadIndex = i; break }
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (leadIndex !== -1) {
|
|
482
|
+
const r = requests[leadIndex]
|
|
483
|
+
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
484
|
+
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
485
|
+
lead = wire(r)
|
|
486
|
+
if (sent) lead.sent = sent
|
|
487
|
+
if (got) lead.got = got
|
|
355
488
|
}
|
|
356
489
|
}
|
|
357
490
|
const failing = []
|
|
@@ -369,37 +502,41 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
369
502
|
else if (navigationOpen && neverFinished(r)) pending.push(i)
|
|
370
503
|
else rest.push(i)
|
|
371
504
|
}
|
|
372
|
-
|
|
505
|
+
// The action's own requests are kept whatever the cap: they are the row's first line.
|
|
506
|
+
const actionSet = new Set(actionIndexes)
|
|
507
|
+
const keep = failing.slice(-MAX_REQUESTS).concat(actionIndexes.slice(0, MAX_NOTE))
|
|
508
|
+
.concat(api.slice(-MAX_REQUESTS))
|
|
373
509
|
.concat(pending.slice(-MAX_REQUESTS)).concat(rest.slice(-2))
|
|
374
510
|
const chosen = []
|
|
375
511
|
const seen = new Set()
|
|
376
512
|
for (const i of keep.sort(function (a, b) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
513
|
+
function rank(index) {
|
|
514
|
+
if (failedRequest(requests[index])) return 0
|
|
515
|
+
return actionSet.has(index) ? 1 : 2
|
|
516
|
+
}
|
|
517
|
+
const left = rank(a)
|
|
518
|
+
const right = rank(b)
|
|
519
|
+
return left === right ? a - b : left - right
|
|
380
520
|
})) {
|
|
381
521
|
const r = requests[i]
|
|
382
522
|
const key = r.method + ' ' + r.path + ' ' + r.status
|
|
383
523
|
if (seen.has(key)) continue
|
|
384
524
|
seen.add(key)
|
|
385
|
-
chosen.push(
|
|
386
|
-
method: r.method,
|
|
387
|
-
path: r.path,
|
|
388
|
-
status: r.status,
|
|
389
|
-
failure: r.failure,
|
|
390
|
-
sent: r.sent,
|
|
391
|
-
got: r.got,
|
|
392
|
-
type: r.type || undefined,
|
|
393
|
-
})
|
|
525
|
+
chosen.push(wire(r))
|
|
394
526
|
}
|
|
395
527
|
const consoleErrors = []
|
|
528
|
+
const consoleSignals = []
|
|
529
|
+
const consoleWarnings = []
|
|
396
530
|
let spec = null
|
|
397
531
|
let line = 0
|
|
398
532
|
for (const entry of trace) {
|
|
399
533
|
if (!entry) continue
|
|
400
|
-
if (entry.type === 'console' &&
|
|
401
|
-
consoleErrors.push(entry.text)
|
|
402
|
-
|
|
534
|
+
if (entry.type === 'console' && typeof entry.text === 'string') {
|
|
535
|
+
if (entry.messageType === 'error') consoleErrors.push(entry.text)
|
|
536
|
+
else if (consoleSignal(entry.text)) consoleSignals.push(entry.text)
|
|
537
|
+
else if (entry.messageType === 'warning' || entry.messageType === 'warn')
|
|
538
|
+
consoleWarnings.push(entry.text)
|
|
539
|
+
} else if (entry.type === 'event' && entry.method === 'pageError') {
|
|
403
540
|
const message = messageOf(entry.params)
|
|
404
541
|
if (message) consoleErrors.push(message)
|
|
405
542
|
} else if (entry.type === 'before' && Array.isArray(entry.stack) && entry.stack.length) {
|
|
@@ -411,15 +548,20 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
411
548
|
}
|
|
412
549
|
}
|
|
413
550
|
// Chromium reports every failed request as a console error too; where the page logged something
|
|
414
|
-
// of its own about it, that is the line worth reading.
|
|
551
|
+
// of its own about it, that is the line worth reading. Errors lead, then the lines that name a
|
|
552
|
+
// session decision or come from a molecule package (m148), then any other warning — the browser's
|
|
553
|
+
// resource error is last, because it is evidence of a failed request, not of a cause, and the
|
|
554
|
+
// platform's own package saying "logging out" must not be ranked below it.
|
|
415
555
|
const specific = consoleErrors.filter(function (text) { return !/^Failed to load resource/.test(text) })
|
|
416
|
-
const keptConsole = specific.
|
|
556
|
+
const keptConsole = specific.concat(consoleSignals, consoleWarnings)
|
|
557
|
+
if (specific.length === 0) keptConsole.push.apply(keptConsole, consoleErrors)
|
|
417
558
|
if (chosen.length === 0 && keptConsole.length === 0 && actions.length === 0) continue
|
|
418
559
|
process.stdout.write('TRACE ' + JSON.stringify({
|
|
419
560
|
dir: path.relative(ROOT, path.dirname(one.file)),
|
|
420
561
|
spec: spec,
|
|
421
562
|
line: line,
|
|
422
563
|
lead: lead,
|
|
564
|
+
lastAction: last,
|
|
423
565
|
// Failures first, per the sort above; the keep-window is the HEAD of that order. Taking the
|
|
424
566
|
// tail here dropped every failed request as soon as a page made more than MAX_REQUESTS of them
|
|
425
567
|
// plus the answered ones (xr-molecule-flash-m112's note listed only -1s and 200s).
|
|
@@ -447,6 +589,31 @@ function parseTraceRequest(value) {
|
|
|
447
589
|
...(typeof r.sent === 'string' && r.sent ? { sent: r.sent } : {}),
|
|
448
590
|
...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
|
|
449
591
|
...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
|
|
592
|
+
...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* One `lastAction` out of a `TRACE` line's json, or null when the value is not one.
|
|
597
|
+
*
|
|
598
|
+
* @param value - The line's `lastAction`.
|
|
599
|
+
* @returns The action and its requests, or null.
|
|
600
|
+
*/
|
|
601
|
+
function parseLastAction(value) {
|
|
602
|
+
if (typeof value !== 'object' || value === null)
|
|
603
|
+
return null;
|
|
604
|
+
const raw = value;
|
|
605
|
+
if (typeof raw.api !== 'string' || !Number.isFinite(Number(raw.start)))
|
|
606
|
+
return null;
|
|
607
|
+
const requests = Array.isArray(raw.requests) ? raw.requests : [];
|
|
608
|
+
return {
|
|
609
|
+
api: raw.api,
|
|
610
|
+
start: Number(raw.start),
|
|
611
|
+
end: raw.end === null || raw.end === undefined ? null : Number(raw.end),
|
|
612
|
+
requests: requests
|
|
613
|
+
.map(parseTraceRequest)
|
|
614
|
+
.filter((request) => request !== null),
|
|
615
|
+
...(typeof raw.pageBefore === 'string' ? { pageBefore: raw.pageBefore } : {}),
|
|
616
|
+
...(typeof raw.pageAfter === 'string' ? { pageAfter: raw.pageAfter } : {}),
|
|
450
617
|
};
|
|
451
618
|
}
|
|
452
619
|
/**
|
|
@@ -469,6 +636,7 @@ export function parseTraceArtifacts(stdout) {
|
|
|
469
636
|
const consoleMessages = Array.isArray(raw.console) ? raw.console : [];
|
|
470
637
|
const actions = Array.isArray(raw.actions) ? raw.actions : [];
|
|
471
638
|
const lead = parseTraceRequest(raw.lead);
|
|
639
|
+
const lastAction = parseLastAction(raw.lastAction);
|
|
472
640
|
out.push({
|
|
473
641
|
spec: typeof raw.spec === 'string' ? raw.spec : null,
|
|
474
642
|
line: typeof raw.line === 'number' ? raw.line : 0,
|
|
@@ -476,6 +644,7 @@ export function parseTraceArtifacts(stdout) {
|
|
|
476
644
|
.map(parseTraceRequest)
|
|
477
645
|
.filter((request) => request !== null),
|
|
478
646
|
...(lead === null ? {} : { lead }),
|
|
647
|
+
...(lastAction === null ? {} : { lastAction }),
|
|
479
648
|
console: consoleMessages.map(String),
|
|
480
649
|
actions: actions
|
|
481
650
|
.filter((a) => typeof a === 'object' && a !== null)
|
|
@@ -536,6 +705,26 @@ function requestLine(request) {
|
|
|
536
705
|
: '';
|
|
537
706
|
return `${head}${sent}${got}`;
|
|
538
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* One request as the note writes it when the body matters as much as the status: the app's own
|
|
710
|
+
* answer, printed to `gotChars`.
|
|
711
|
+
*
|
|
712
|
+
* @param request - The request.
|
|
713
|
+
* @param gotChars - How much of the response body to print.
|
|
714
|
+
* @returns The request's clause.
|
|
715
|
+
*/
|
|
716
|
+
function requestWithBody(request, gotChars) {
|
|
717
|
+
const status = request.status > 0
|
|
718
|
+
? String(request.status)
|
|
719
|
+
: request.failure
|
|
720
|
+
? `no response (${bodyLine(redactSecrets(request.failure), TRACE_NOTE_FAILURE_CHARS)})`
|
|
721
|
+
: 'no response';
|
|
722
|
+
const sent = request.sent
|
|
723
|
+
? ` sent ${bodyLine(redactSecrets(request.sent), TRACE_NOTE_SENT_CHARS)}`
|
|
724
|
+
: '';
|
|
725
|
+
const got = request.got ? ` got ${bodyLine(redactSecrets(request.got), gotChars)}` : '';
|
|
726
|
+
return `${requestLabel(request)} → ${status}${sent}${got}`;
|
|
727
|
+
}
|
|
539
728
|
/**
|
|
540
729
|
* The failing step's own request as the note writes it first: `the failing step sent DELETE
|
|
541
730
|
* /api/profiles/x/follow → 200 got {"profile":{…}}`.
|
|
@@ -548,18 +737,27 @@ function requestLine(request) {
|
|
|
548
737
|
* @returns The clause the test's row opens with.
|
|
549
738
|
*/
|
|
550
739
|
function leadRequestLine(request) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
740
|
+
return `the failing step sent ${requestWithBody(request, TRACE_NOTE_LEAD_GOT_CHARS)}`;
|
|
741
|
+
}
|
|
742
|
+
/** A request's identity in a trace: method, path and status. */
|
|
743
|
+
function requestKey(request) {
|
|
744
|
+
return `${request.method} ${request.path} ${request.status}`;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* When an action ran, relative to the first action the trace recorded — `+2.9 s`.
|
|
748
|
+
*
|
|
749
|
+
* The trace's clock starts with the worker process, so the raw value is meaningless to a reader;
|
|
750
|
+
* a relative time keeps the trace's own ordering visible without inventing a wall-clock time.
|
|
751
|
+
*
|
|
752
|
+
* @param item - The trace.
|
|
753
|
+
* @param start - The action's start, on the trace's clock.
|
|
754
|
+
* @returns The time to print.
|
|
755
|
+
*/
|
|
756
|
+
function actionClock(item, start) {
|
|
757
|
+
const first = item.actions?.[0]?.start;
|
|
758
|
+
if (first === undefined || !Number.isFinite(first) || start < first)
|
|
759
|
+
return `${Math.round(start)} ms`;
|
|
760
|
+
return `+${((start - first) / 1000).toFixed(1)} s`;
|
|
563
761
|
}
|
|
564
762
|
/** Whether a sent body is an object whose only top-level key is `data` — the wrapped-body trap. */
|
|
565
763
|
function wrappedInData(text) {
|
|
@@ -589,6 +787,16 @@ function wrappedInData(text) {
|
|
|
589
787
|
* invisible to the next proof run. The note is about the test's own API calls.
|
|
590
788
|
*/
|
|
591
789
|
const STATIC_ASSET_PATH = /^\/(?:sw|service-worker)\.js(?:[?#]|$)|\/manifest\.webmanifest(?:[?#]|$)|\/favicon[^/]*(?:[?#]|$)|\/fonts?\//i;
|
|
790
|
+
/**
|
|
791
|
+
* The PWA worker's own hashed script, the manifest's touch icons and the splash images.
|
|
792
|
+
*
|
|
793
|
+
* The worker and the shell type these `other`/`fetch`, not `script`/`image`, so
|
|
794
|
+
* {@link STATIC_ASSET_TYPES} alone never caught them: on xr-molecule-flash-m148 every failing
|
|
795
|
+
* test's row filled three of its four request slots with `GET /workbox-d8ed3718.js → 200` and
|
|
796
|
+
* `GET /icons/splash-1125x2436.svg → 200`, and the register `POST /api/users` that explains the
|
|
797
|
+
* signed-out page never appeared.
|
|
798
|
+
*/
|
|
799
|
+
const SHELL_FILE_PATH = /^\/workbox-[^/]*\.js(?:[?#]|$)|^\/icons\/|^\/(?:apple-touch-icon|og-image|splash)[^/]*(?:[?#]|$)/i;
|
|
592
800
|
/** The build/dev output paths the page shell loads (Vite's bundle, the platform's own files). */
|
|
593
801
|
const STATIC_ASSET_DIR = /^\/(?:assets|@vite|__mol)\//i;
|
|
594
802
|
/** The resource types a page loads for itself rather than for the step under test. */
|
|
@@ -620,7 +828,7 @@ export function isStaticAssetRequest(request) {
|
|
|
620
828
|
if (request.type !== undefined && STATIC_ASSET_TYPES.has(request.type))
|
|
621
829
|
return true;
|
|
622
830
|
const path = requestPath(request);
|
|
623
|
-
return STATIC_ASSET_PATH.test(path) || STATIC_ASSET_DIR.test(path);
|
|
831
|
+
return STATIC_ASSET_PATH.test(path) || STATIC_ASSET_DIR.test(path) || SHELL_FILE_PATH.test(path);
|
|
624
832
|
}
|
|
625
833
|
/**
|
|
626
834
|
* Whether a trace recorded a request the server actually failed: a 4xx/5xx, or a network error.
|
|
@@ -728,7 +936,7 @@ function repeatsEarlierFailure(item, earlier) {
|
|
|
728
936
|
}
|
|
729
937
|
/**
|
|
730
938
|
* Whether a trace holds evidence the note can explain: a request that got a RECORDED response, a
|
|
731
|
-
* real failure (4xx/5xx or a network error), or a console
|
|
939
|
+
* real failure (4xx/5xx or a network error), or a console line. A request with no response is
|
|
732
940
|
* not evidence (m112 +53:46): four of the note's five rows were traces of one `afterEach` line
|
|
733
941
|
* whose every request was `-1`, so the favorite cause — repeated in all three walks — stayed
|
|
734
942
|
* hidden in "… and 7 more traced failures".
|
|
@@ -739,7 +947,7 @@ function repeatsEarlierFailure(item, earlier) {
|
|
|
739
947
|
* `page.goto` timed out ({@link stalledDocumentSubresource}).
|
|
740
948
|
*
|
|
741
949
|
* @param item - One traced failure.
|
|
742
|
-
* @returns True when the trace holds a request or console
|
|
950
|
+
* @returns True when the trace holds a request or console line worth a row.
|
|
743
951
|
*/
|
|
744
952
|
function traceRowEvidence(item) {
|
|
745
953
|
return (item.console.length > 0 ||
|
|
@@ -918,14 +1126,19 @@ function missingApiPrefixSentence(request) {
|
|
|
918
1126
|
* line rather than inferred from the status; when one of those bodies is an object whose only key
|
|
919
1127
|
* is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
|
|
920
1128
|
*
|
|
921
|
-
* Every row opens with the
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
1129
|
+
* Every row opens with the test's LAST user action before the failing assertion and the requests
|
|
1130
|
+
* that action caused ({@link TraceArtifact.lastAction}) — started after it, before the assertion,
|
|
1131
|
+
* the app's own API calls first, each with its status and the first 200 characters of its response,
|
|
1132
|
+
* then where the page went next. The trace's action clock and each request's `_monotonicTime` are
|
|
1133
|
+
* the same clock, so "caused" is the trace's own fact rather than a guess from recording order.
|
|
1134
|
+
* xr-molecule-flash-m148 failed 13 then 12 tests on one unnamed cause while every row led with
|
|
1135
|
+
* `GET /api/tags → 200` — the Home page's own fetch after the redirect — and filled its answered
|
|
1136
|
+
* slots with the PWA's `/workbox-*.js` and its `/icons/*.svg`; the register `POST /api/users`
|
|
1137
|
+
* (answered 201, the page still rendering signed-out) never appeared. With no action clock, the
|
|
1138
|
+
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
926
1139
|
*
|
|
927
1140
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
928
|
-
* needs a recorded response, a real failure or a console
|
|
1141
|
+
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
929
1142
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
930
1143
|
* earlier walk → the rest ({@link traceRowTier}). Without that the note spent all five rows on four
|
|
931
1144
|
* traces of one `afterEach` line whose every request was `-1`, and left the favorite cause — the
|
|
@@ -1009,17 +1222,20 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1009
1222
|
];
|
|
1010
1223
|
const rows = [];
|
|
1011
1224
|
for (const [index, item] of traced.entries()) {
|
|
1012
|
-
// The
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1225
|
+
// The test's LAST action before the failing assertion leads the row (m148): the requests it
|
|
1226
|
+
// caused and what came back explain a page that never showed the signed-in state, while the
|
|
1227
|
+
// page's own last call (a redirect target's `GET /api/tags`) and the shell's files do not.
|
|
1015
1228
|
const lead = item.lead;
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1229
|
+
const step = item.lastAction;
|
|
1230
|
+
// A navigation that never finished has one explanation — the stalled asset it was waiting on
|
|
1231
|
+
// (m135) — and that leads the row instead of the action's own document request.
|
|
1232
|
+
const stalledNav = navigationNeverFinished(item) && item.requests.some(stalledDocumentSubresource);
|
|
1233
|
+
const stepRequests = step !== undefined && !stalledNav ? step.requests : [];
|
|
1234
|
+
const stepKeys = new Set(stepRequests.map(requestKey));
|
|
1235
|
+
const isLead = (request) => lead !== undefined && requestKey(request) === requestKey(lead);
|
|
1020
1236
|
// The page shell's own requests take no row, in any bucket: an aborted `/sw.js` would
|
|
1021
1237
|
// otherwise be printed as an "answered" request with no response (m125 +29:33).
|
|
1022
|
-
const requests = item.requests.filter((request) => !isStaticAssetRequest(request) && !isLead(request));
|
|
1238
|
+
const requests = item.requests.filter((request) => !isStaticAssetRequest(request) && !isLead(request) && !stepKeys.has(requestKey(request)));
|
|
1023
1239
|
const failed = requests.filter(failedRequest);
|
|
1024
1240
|
const unanswered = requests.filter(unansweredRequest);
|
|
1025
1241
|
const answered = requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
|
|
@@ -1030,7 +1246,27 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1030
1246
|
: [];
|
|
1031
1247
|
let room = MAX_TRACE_NOTE_REQUESTS;
|
|
1032
1248
|
const parts = [];
|
|
1033
|
-
if (
|
|
1249
|
+
if (step !== undefined && !stalledNav) {
|
|
1250
|
+
const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
|
|
1251
|
+
if (step.requests.length === 0) {
|
|
1252
|
+
parts.push(`${head}, and it sent no request`);
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
const shown = step.requests.slice(0, room);
|
|
1256
|
+
room -= shown.length;
|
|
1257
|
+
// The response body is the point (m148: the register `POST /api/users` answered 201 and
|
|
1258
|
+
// the page rendered signed-out anyway) — printed whatever the status.
|
|
1259
|
+
parts.push(`${head}, and it sent ${shown.map((request) => requestWithBody(request, TRACE_NOTE_LEAD_GOT_CHARS)).join('; ')}`);
|
|
1260
|
+
}
|
|
1261
|
+
// What the page did next: a navigation shows in the main frame's URL even when the app moved
|
|
1262
|
+
// client-side and the network log recorded no document request.
|
|
1263
|
+
if (step.pageAfter !== undefined) {
|
|
1264
|
+
parts.push(step.pageBefore !== undefined && step.pageBefore !== step.pageAfter
|
|
1265
|
+
? `the page then navigated to ${step.pageAfter}`
|
|
1266
|
+
: `the page stayed at ${step.pageAfter}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
else if (lead !== undefined) {
|
|
1034
1270
|
parts.push(leadRequestLine(lead));
|
|
1035
1271
|
room -= 1;
|
|
1036
1272
|
}
|
|
@@ -1052,7 +1288,12 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1052
1288
|
shown.map(requestLabel).join('; '));
|
|
1053
1289
|
}
|
|
1054
1290
|
if (answered.length > 0 && room > 0) {
|
|
1055
|
-
|
|
1291
|
+
// An action that sent nothing leaves the page's own load as the only requests there are:
|
|
1292
|
+
// name them for what they are, not as the action's answer.
|
|
1293
|
+
const label = step !== undefined && !stalledNav && step.requests.length === 0
|
|
1294
|
+
? 'also answered (started before the action)'
|
|
1295
|
+
: 'answered';
|
|
1296
|
+
parts.push(`${label}: ${answered.slice(0, room).map(requestLine).join('; ')}`);
|
|
1056
1297
|
}
|
|
1057
1298
|
if (item.console.length > 0)
|
|
1058
1299
|
parts.push(`console: ${item.console
|
|
@@ -1104,8 +1345,9 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1104
1345
|
return (transportLead +
|
|
1105
1346
|
stalledLead +
|
|
1106
1347
|
`The platform read the failing tests' own Playwright traces from the run against ${target} ` +
|
|
1107
|
-
`(what each page sent and got — first
|
|
1108
|
-
`requests that failed, the ones that got no response, and its last answered
|
|
1348
|
+
`(what each page sent and got — first what the test's last action sent and where the page went ` +
|
|
1349
|
+
`next, then the requests that failed, the ones that got no response, and its last answered ` +
|
|
1350
|
+
`ones):\n` +
|
|
1109
1351
|
`${rows.join('\n')}${more}\n` +
|
|
1110
1352
|
(anyFailed
|
|
1111
1353
|
? everyFailureLacksPrefix
|
|
@@ -1119,10 +1361,12 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1119
1361
|
'page-state bug: re-run these tests against the same URL first, and change the app only ' +
|
|
1120
1362
|
'if the same navigation times out again with no stalled asset in the trace.'
|
|
1121
1363
|
: 'No listed request failed — none got a 4xx/5xx or a network error — so the cause is in the ' +
|
|
1122
|
-
|
|
1123
|
-
'row)
|
|
1124
|
-
'
|
|
1125
|
-
'
|
|
1364
|
+
"page, not the API. Start with what the test's last action sent (the first line of each " +
|
|
1365
|
+
'row) and what the page did next: a request that answered (2xx included) while the page ' +
|
|
1366
|
+
'still did not show the state the assertion waited on is the fact to read — its status ' +
|
|
1367
|
+
'and response body, not the selector. If the request is not the one the test meant — an ' +
|
|
1368
|
+
'unfollow sent where the test clicked follow — the state the action read was stale ' +
|
|
1369
|
+
'before it.') +
|
|
1126
1370
|
prefixAdvice +
|
|
1127
1371
|
(wrapped
|
|
1128
1372
|
? ' The API received a body wrapped in `data`: app-http’s `post(url, data, config)` / ' +
|