@molecule/api-ai-tools 1.9.9 → 1.9.10
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 +56 -8
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +62 -2
- package/dist/trace-artifacts.d.ts +59 -10
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +300 -82
- 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-18T06:52:09.924Z
|
|
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
|
|
|
@@ -428,17 +466,27 @@ 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
|
/** The console errors and page errors the page raised. */
|
|
441
481
|
console: string[]
|
|
482
|
+
/**
|
|
483
|
+
* The test's LAST user action before the failing assertion and the requests it caused — what the
|
|
484
|
+
* row leads with, when the trace carries the action times ({@link TraceLastAction}).
|
|
485
|
+
*
|
|
486
|
+
* Absent on traces read before this field existed, and on traces whose network entries recorded
|
|
487
|
+
* no clock; the row then falls back to {@link lead}.
|
|
488
|
+
*/
|
|
489
|
+
lastAction?: TraceLastAction
|
|
442
490
|
/**
|
|
443
491
|
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
|
|
444
492
|
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
|
|
@@ -1900,8 +1948,8 @@ A 4xx request carries what the page SENT beside its status, so a wrapped body is
|
|
|
1900
1948
|
line rather than inferred from the status; when one of those bodies is an object whose only key
|
|
1901
1949
|
is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
|
|
1902
1950
|
|
|
1903
|
-
Every row opens with the
|
|
1904
|
-
|
|
1951
|
+
Every row opens with the test's LAST user action before the failing assertion and the requests
|
|
1952
|
+
that action caused ({@link TraceArtifact.lastAction}) — started after it, before the assertion,
|
|
1905
1953
|
|
|
1906
1954
|
_(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts`.)_
|
|
1907
1955
|
|
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 {
|
|
@@ -79,17 +113,27 @@ 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
|
/** The console errors and page errors the page raised. */
|
|
92
128
|
console: string[];
|
|
129
|
+
/**
|
|
130
|
+
* The test's LAST user action before the failing assertion and the requests it caused — what the
|
|
131
|
+
* row leads with, when the trace carries the action times ({@link TraceLastAction}).
|
|
132
|
+
*
|
|
133
|
+
* Absent on traces read before this field existed, and on traces whose network entries recorded
|
|
134
|
+
* no clock; the row then falls back to {@link lead}.
|
|
135
|
+
*/
|
|
136
|
+
lastAction?: TraceLastAction;
|
|
93
137
|
/**
|
|
94
138
|
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
|
|
95
139
|
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
|
|
@@ -226,11 +270,16 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
226
270
|
* line rather than inferred from the status; when one of those bodies is an object whose only key
|
|
227
271
|
* is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
|
|
228
272
|
*
|
|
229
|
-
* Every row opens with the
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
273
|
+
* Every row opens with the test's LAST user action before the failing assertion and the requests
|
|
274
|
+
* that action caused ({@link TraceArtifact.lastAction}) — started after it, before the assertion,
|
|
275
|
+
* the app's own API calls first, each with its status and the first 200 characters of its response,
|
|
276
|
+
* then where the page went next. The trace's action clock and each request's `_monotonicTime` are
|
|
277
|
+
* the same clock, so "caused" is the trace's own fact rather than a guess from recording order.
|
|
278
|
+
* xr-molecule-flash-m148 failed 13 then 12 tests on one unnamed cause while every row led with
|
|
279
|
+
* `GET /api/tags → 200` — the Home page's own fetch after the redirect — and filled its answered
|
|
280
|
+
* slots with the PWA's `/workbox-*.js` and its `/icons/*.svg`; the register `POST /api/users`
|
|
281
|
+
* (answered 201, the page still rendering signed-out) never appeared. With no action clock, the
|
|
282
|
+
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
234
283
|
*
|
|
235
284
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
236
285
|
* needs a recorded response, a real failure or a console error ({@link traceRowEvidence}), rows are
|
|
@@ -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
|
@@ -90,6 +90,7 @@ const zlib = require('node:zlib')
|
|
|
90
90
|
const ROOT = process.argv[2] || '/workspace'
|
|
91
91
|
const MAX_TRACES = ${MAX_TRACE_ARTIFACTS}
|
|
92
92
|
const MAX_REQUESTS = ${MAX_TRACED_REQUESTS}
|
|
93
|
+
const MAX_NOTE = ${MAX_TRACE_NOTE_REQUESTS}
|
|
93
94
|
const MAX_CONSOLE = ${MAX_TRACED_CONSOLE}
|
|
94
95
|
const MAX_ACTIONS = ${MAX_TRACED_ACTIONS}
|
|
95
96
|
const MAX_BODY = ${MAX_TRACED_BODY_CHARS}
|
|
@@ -231,6 +232,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
231
232
|
status: Number(response.status) || 0,
|
|
232
233
|
failure: typeof response._failureText === 'string' ? response._failureText : undefined,
|
|
233
234
|
type: type,
|
|
235
|
+
// The HAR entry carries the monotonic clock the trace's action entries use, so a request
|
|
236
|
+
// can be attributed to the action that caused it (see \`lastAction\` below).
|
|
237
|
+
startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
|
|
234
238
|
postData: request.postData,
|
|
235
239
|
content: response.content,
|
|
236
240
|
})
|
|
@@ -240,16 +244,6 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
240
244
|
origin = first ? originOf(first.path) : ''
|
|
241
245
|
}
|
|
242
246
|
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
247
|
// The page's own actions, timed. before/after pair on the call id, and only the LIBRARY
|
|
254
248
|
// traces carry a class and a method (Frame.goto, Locator.fill); the test runner's own
|
|
255
249
|
// trace times its hooks and fixtures with class Test, which is not a page action. Read BEFORE
|
|
@@ -283,20 +277,36 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
283
277
|
// model to write a caching service worker into the app). A document's OWN stylesheet or
|
|
284
278
|
// script is not shell noise: the navigation blocks on it.
|
|
285
279
|
const SHELL_TYPE = { font: 1, image: 1, stylesheet: 1, script: 1, manifest: 1 }
|
|
280
|
+
// The PWA worker's own hashed script (\`/workbox-d8ed3718.js\`), the manifest's touch icons
|
|
281
|
+
// (\`/icons/splash-*.svg\`), the favicons and fonts: the shell's files. They are typed \`other\` or
|
|
282
|
+
// \`fetch\` by the worker and the shell rather than \`script\`/\`image\`, so the type alone never
|
|
283
|
+
// caught them — on xr-molecule-flash-m148 they took three of the note's four slots on every
|
|
284
|
+
// failing test and pushed the register \`POST /api/users\` out.
|
|
286
285
|
function shellPath(path) {
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
var p = path
|
|
287
|
+
try { p = new URL(path, 'http://trace.local').pathname } catch (e) { p = String(path).split(/[?#]/)[0] }
|
|
288
|
+
return /^\\/(?:sw|service-worker)\\.js(?:[?#]|$)|\\/manifest\\.webmanifest(?:[?#]|$)|\\/favicon[^/]*(?:[?#]|$)|\\/fonts?\\//.test(p) ||
|
|
289
|
+
/^\\/(?:assets|@vite|__mol)\\//.test(p) ||
|
|
290
|
+
/^\\/workbox-[^/]*\\.js(?:[?#]|$)/.test(p) ||
|
|
291
|
+
/^\\/icons\\//.test(p) ||
|
|
292
|
+
/^\\/(?:apple-touch-icon|og-image|splash)[^/]*(?:[?#]|$)/.test(p)
|
|
289
293
|
}
|
|
290
294
|
function shellRequest(r) {
|
|
291
295
|
if (r.type === 'document') return false
|
|
292
296
|
return SHELL_TYPE[r.type] === 1 || shellPath(r.path)
|
|
293
297
|
}
|
|
294
298
|
// The shell requests that stay noise even when they never finished: the PWA worker, the
|
|
295
|
-
// manifest, a favicon, a font
|
|
299
|
+
// manifest, a favicon, a font, the worker's own script and the manifest's icons. A pending
|
|
300
|
+
// \`/assets/*.css\` is NOT one of these — the navigation blocks on it (m135).
|
|
296
301
|
function incidentalShell(r) {
|
|
297
302
|
if (r.type === 'document') return false
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
if (r.type === 'font' || r.type === 'manifest') return true
|
|
304
|
+
var p = r.path
|
|
305
|
+
try { p = new URL(r.path, 'http://trace.local').pathname } catch (e) { p = String(r.path).split(/[?#]/)[0] }
|
|
306
|
+
return /^\\/(?:sw|service-worker)\\.js(?:[?#]|$)|\\/manifest\\.webmanifest(?:[?#]|$)|\\/favicon[^/]*(?:[?#]|$)|\\/fonts?\\//.test(p) ||
|
|
307
|
+
/^\\/workbox-[^/]*\\.js(?:[?#]|$)/.test(p) ||
|
|
308
|
+
/^\\/icons\\//.test(p) ||
|
|
309
|
+
/^\\/(?:apple-touch-icon|og-image|splash)[^/]*(?:[?#]|$)/.test(p)
|
|
300
310
|
}
|
|
301
311
|
// A failed request: a 4xx/5xx, or no response at all WITH Playwright's error text (a refused
|
|
302
312
|
// connection). No response and no text is an aborted/pending request, not a failure. A 404 on
|
|
@@ -309,15 +319,19 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
309
319
|
function neverFinished(r) {
|
|
310
320
|
return r.status <= 0 && typeof r.failure !== 'string'
|
|
311
321
|
}
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
322
|
+
// Whether a request can lead a row: it got an answer (or failed for its own recorded reason),
|
|
323
|
+
// and it is the app's own call — a fetch/xhr of the page, or its document — never the page
|
|
324
|
+
// shell's own probe (a refused \`/sw.js\` is not the step's request — m125 +29:33).
|
|
325
|
+
//
|
|
326
|
+
// It ranks the requests the test's last action caused (see \`lastAction\` below), and it is the
|
|
327
|
+
// whole rule on a trace with no action clock (the fallback lead: the page's LAST call to the
|
|
328
|
+
// app's own origin, else the document load). That fallback is right for a click that changed
|
|
329
|
+
// nothing — xr-molecule-flash-m135 clicked Follow and sent a DELETE (its \`following\` state was
|
|
330
|
+
// stale) — and wrong for an assertion after a navigation (m148), which is why the action's
|
|
331
|
+
// clock, when the trace has one, decides.
|
|
317
332
|
//
|
|
318
333
|
// 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).
|
|
334
|
+
// what came back, and the note has its own honest row for it (m112 +30:45).
|
|
321
335
|
function leadCandidate(r) {
|
|
322
336
|
if (r.status <= 0 && typeof r.failure !== 'string') return false
|
|
323
337
|
if (r.type === 'document') return true
|
|
@@ -329,29 +343,131 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
329
343
|
try { p = new URL(r.path, 'http://trace.local').pathname } catch (e) { p = String(r.path).split(/[?#]/)[0] }
|
|
330
344
|
return !shellPath(p)
|
|
331
345
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
// The test's LAST user action before the failing assertion, and the requests it caused.
|
|
347
|
+
//
|
|
348
|
+
// The old lead was "the page's LAST call to the app's own origin", which is right for a click
|
|
349
|
+
// that changed nothing (m135) and wrong for an assertion after a navigation: the last call
|
|
350
|
+
// belongs to the page the app landed on (xr-molecule-flash-m148's \`GET /api/tags\` on Home),
|
|
351
|
+
// not to the action the test took. The trace records both clocks — the action's \`startTime\`
|
|
352
|
+
// and each HAR entry's \`_monotonicTime\` — so the requests that started after the last action
|
|
353
|
+
// are exactly the ones it caused, and the register \`POST /api/users\` (answered 201, the page
|
|
354
|
+
// then rendering signed-out) leads instead of the Home page's own fetch.
|
|
355
|
+
const USER_ACTION = /\\.(?:click|dblclick|tap|fill|press|pressSequentially|selectOption|check|uncheck|setInputFiles|goto|type)$/
|
|
356
|
+
const userActions = actions.filter(function (a) { return USER_ACTION.test(a.api) })
|
|
357
|
+
const lastAction = userActions.length > 0 ? userActions[userActions.length - 1] : null
|
|
358
|
+
// A request the action caused started after it (a millisecond of clock slack) and before the
|
|
359
|
+
// assertion — which is everything later in the trace. A request that started before it is the
|
|
360
|
+
// page's own load, not the action's; the page shell's files are never the step's, except a
|
|
361
|
+
// document subresource the open navigation is waiting on (m135).
|
|
362
|
+
function causedIndexes(action) {
|
|
363
|
+
const out = []
|
|
364
|
+
for (let i = 0; i < requests.length; i++) {
|
|
365
|
+
const r = requests[i]
|
|
366
|
+
if (typeof r.startedAt !== 'number' || r.startedAt < action.start - 1) continue
|
|
367
|
+
if (shellRequest(r) && !(navigationOpen && neverFinished(r) && !incidentalShell(r))) continue
|
|
368
|
+
out.push(i)
|
|
369
|
+
}
|
|
370
|
+
return out
|
|
336
371
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
372
|
+
// API requests first, then the navigation the page made; a stalled asset outranks both when the
|
|
373
|
+
// navigation it belongs to never finished (m135's goto timeout was waiting on the CSS).
|
|
374
|
+
function causedOrder(indexes) {
|
|
375
|
+
const stalled = []
|
|
376
|
+
const apiCalls = []
|
|
377
|
+
const documents = []
|
|
378
|
+
const other = []
|
|
379
|
+
for (const i of indexes) {
|
|
380
|
+
const r = requests[i]
|
|
381
|
+
if (r.type === 'document') documents.push(i)
|
|
382
|
+
else if (navigationOpen && neverFinished(r) && !incidentalShell(r)) stalled.push(i)
|
|
383
|
+
else if (r.type === 'fetch' || r.type === 'xhr') apiCalls.push(i)
|
|
384
|
+
else other.push(i)
|
|
340
385
|
}
|
|
386
|
+
return stalled.concat(apiCalls, documents, other)
|
|
341
387
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
388
|
+
const actionIndexes = lastAction === null ? [] : causedOrder(causedIndexes(lastAction))
|
|
389
|
+
// Both the action's own requests and a 4xx from the app's own API keep what the page sent and
|
|
390
|
+
// what came back. The action's requests keep the RESPONSE whatever its status — an answered
|
|
391
|
+
// \`POST /api/users → 201\` and its body is the fact that explains a signed-out page.
|
|
392
|
+
for (const i of actionIndexes) {
|
|
393
|
+
const r = requests[i]
|
|
394
|
+
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
395
|
+
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
396
|
+
if (sent) r.sent = sent
|
|
397
|
+
if (got) r.got = got
|
|
398
|
+
}
|
|
399
|
+
for (const r of requests) {
|
|
400
|
+
if (r.status < 400 || r.status >= 500) continue
|
|
401
|
+
if (/^https?:/i.test(r.path)) continue
|
|
345
402
|
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
346
403
|
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
347
|
-
|
|
404
|
+
if (sent) r.sent = sent
|
|
405
|
+
if (got) r.got = got
|
|
406
|
+
}
|
|
407
|
+
function wire(r) {
|
|
408
|
+
return {
|
|
348
409
|
method: r.method,
|
|
349
410
|
path: r.path,
|
|
350
411
|
status: r.status,
|
|
351
412
|
failure: r.failure,
|
|
413
|
+
...(r.sent ? { sent: r.sent } : {}),
|
|
414
|
+
...(r.got ? { got: r.got } : {}),
|
|
352
415
|
type: r.type || undefined,
|
|
353
|
-
|
|
354
|
-
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
|
|
419
|
+
// The trace records these around every action, which is how the note can say what the page did
|
|
420
|
+
// after the request — a client-side redirect never appears in the network log as a document.
|
|
421
|
+
const frames = []
|
|
422
|
+
for (const entry of trace) {
|
|
423
|
+
if (!entry || entry.type !== 'frame-snapshot') continue
|
|
424
|
+
const snap = entry.snapshot || {}
|
|
425
|
+
if (snap.isMainFrame === false) continue
|
|
426
|
+
if (typeof snap.frameUrl !== 'string' || typeof snap.timestamp !== 'number') continue
|
|
427
|
+
frames.push({ url: pathOf(snap.frameUrl).slice(0, MAX_URL), at: snap.timestamp })
|
|
428
|
+
}
|
|
429
|
+
let last = null
|
|
430
|
+
if (lastAction !== null) {
|
|
431
|
+
let pageBefore
|
|
432
|
+
for (const frame of frames) { if (frame.at <= lastAction.start) pageBefore = frame.url }
|
|
433
|
+
if (pageBefore === undefined && frames.length > 0) pageBefore = frames[0].url
|
|
434
|
+
const pageAfter = frames.length > 0 ? frames[frames.length - 1].url : undefined
|
|
435
|
+
last = {
|
|
436
|
+
api: lastAction.api,
|
|
437
|
+
start: lastAction.start,
|
|
438
|
+
end: lastAction.end,
|
|
439
|
+
requests: actionIndexes.slice(0, MAX_NOTE).map(function (i) { return wire(requests[i]) }),
|
|
440
|
+
...(pageBefore === undefined ? {} : { pageBefore: pageBefore }),
|
|
441
|
+
...(pageAfter === undefined ? {} : { pageAfter: pageAfter }),
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// The lead stays for traces that carry no action clock: the last answered fetch/xhr, else the
|
|
445
|
+
// document. With an action, its first answered request is the lead.
|
|
446
|
+
let lead = null
|
|
447
|
+
for (const i of actionIndexes) {
|
|
448
|
+
if (leadCandidate(requests[i])) { lead = wire(requests[i]); break }
|
|
449
|
+
}
|
|
450
|
+
// The fallback lead stays for traces with no action clock. With an action, only a request it
|
|
451
|
+
// caused is the lead; an action that sent none leaves the page's own load to the row's
|
|
452
|
+
// "also answered" bucket, not to a lead that would hide it.
|
|
453
|
+
if (lead === null && lastAction === null) {
|
|
454
|
+
let leadIndex = -1
|
|
455
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
456
|
+
const r = requests[i]
|
|
457
|
+
if ((r.type === 'fetch' || r.type === 'xhr') && leadCandidate(r)) { leadIndex = i; break }
|
|
458
|
+
}
|
|
459
|
+
if (leadIndex === -1) {
|
|
460
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
461
|
+
if (requests[i].type === 'document' && leadCandidate(requests[i])) { leadIndex = i; break }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (leadIndex !== -1) {
|
|
465
|
+
const r = requests[leadIndex]
|
|
466
|
+
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
467
|
+
const got = bodyText(buf, entries, r.content, MAX_BODY)
|
|
468
|
+
lead = wire(r)
|
|
469
|
+
if (sent) lead.sent = sent
|
|
470
|
+
if (got) lead.got = got
|
|
355
471
|
}
|
|
356
472
|
}
|
|
357
473
|
const failing = []
|
|
@@ -369,28 +485,27 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
369
485
|
else if (navigationOpen && neverFinished(r)) pending.push(i)
|
|
370
486
|
else rest.push(i)
|
|
371
487
|
}
|
|
372
|
-
|
|
488
|
+
// The action's own requests are kept whatever the cap: they are the row's first line.
|
|
489
|
+
const actionSet = new Set(actionIndexes)
|
|
490
|
+
const keep = failing.slice(-MAX_REQUESTS).concat(actionIndexes.slice(0, MAX_NOTE))
|
|
491
|
+
.concat(api.slice(-MAX_REQUESTS))
|
|
373
492
|
.concat(pending.slice(-MAX_REQUESTS)).concat(rest.slice(-2))
|
|
374
493
|
const chosen = []
|
|
375
494
|
const seen = new Set()
|
|
376
495
|
for (const i of keep.sort(function (a, b) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
496
|
+
function rank(index) {
|
|
497
|
+
if (failedRequest(requests[index])) return 0
|
|
498
|
+
return actionSet.has(index) ? 1 : 2
|
|
499
|
+
}
|
|
500
|
+
const left = rank(a)
|
|
501
|
+
const right = rank(b)
|
|
502
|
+
return left === right ? a - b : left - right
|
|
380
503
|
})) {
|
|
381
504
|
const r = requests[i]
|
|
382
505
|
const key = r.method + ' ' + r.path + ' ' + r.status
|
|
383
506
|
if (seen.has(key)) continue
|
|
384
507
|
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
|
-
})
|
|
508
|
+
chosen.push(wire(r))
|
|
394
509
|
}
|
|
395
510
|
const consoleErrors = []
|
|
396
511
|
let spec = null
|
|
@@ -420,6 +535,7 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
420
535
|
spec: spec,
|
|
421
536
|
line: line,
|
|
422
537
|
lead: lead,
|
|
538
|
+
lastAction: last,
|
|
423
539
|
// Failures first, per the sort above; the keep-window is the HEAD of that order. Taking the
|
|
424
540
|
// tail here dropped every failed request as soon as a page made more than MAX_REQUESTS of them
|
|
425
541
|
// plus the answered ones (xr-molecule-flash-m112's note listed only -1s and 200s).
|
|
@@ -447,6 +563,31 @@ function parseTraceRequest(value) {
|
|
|
447
563
|
...(typeof r.sent === 'string' && r.sent ? { sent: r.sent } : {}),
|
|
448
564
|
...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
|
|
449
565
|
...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
|
|
566
|
+
...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* One `lastAction` out of a `TRACE` line's json, or null when the value is not one.
|
|
571
|
+
*
|
|
572
|
+
* @param value - The line's `lastAction`.
|
|
573
|
+
* @returns The action and its requests, or null.
|
|
574
|
+
*/
|
|
575
|
+
function parseLastAction(value) {
|
|
576
|
+
if (typeof value !== 'object' || value === null)
|
|
577
|
+
return null;
|
|
578
|
+
const raw = value;
|
|
579
|
+
if (typeof raw.api !== 'string' || !Number.isFinite(Number(raw.start)))
|
|
580
|
+
return null;
|
|
581
|
+
const requests = Array.isArray(raw.requests) ? raw.requests : [];
|
|
582
|
+
return {
|
|
583
|
+
api: raw.api,
|
|
584
|
+
start: Number(raw.start),
|
|
585
|
+
end: raw.end === null || raw.end === undefined ? null : Number(raw.end),
|
|
586
|
+
requests: requests
|
|
587
|
+
.map(parseTraceRequest)
|
|
588
|
+
.filter((request) => request !== null),
|
|
589
|
+
...(typeof raw.pageBefore === 'string' ? { pageBefore: raw.pageBefore } : {}),
|
|
590
|
+
...(typeof raw.pageAfter === 'string' ? { pageAfter: raw.pageAfter } : {}),
|
|
450
591
|
};
|
|
451
592
|
}
|
|
452
593
|
/**
|
|
@@ -469,6 +610,7 @@ export function parseTraceArtifacts(stdout) {
|
|
|
469
610
|
const consoleMessages = Array.isArray(raw.console) ? raw.console : [];
|
|
470
611
|
const actions = Array.isArray(raw.actions) ? raw.actions : [];
|
|
471
612
|
const lead = parseTraceRequest(raw.lead);
|
|
613
|
+
const lastAction = parseLastAction(raw.lastAction);
|
|
472
614
|
out.push({
|
|
473
615
|
spec: typeof raw.spec === 'string' ? raw.spec : null,
|
|
474
616
|
line: typeof raw.line === 'number' ? raw.line : 0,
|
|
@@ -476,6 +618,7 @@ export function parseTraceArtifacts(stdout) {
|
|
|
476
618
|
.map(parseTraceRequest)
|
|
477
619
|
.filter((request) => request !== null),
|
|
478
620
|
...(lead === null ? {} : { lead }),
|
|
621
|
+
...(lastAction === null ? {} : { lastAction }),
|
|
479
622
|
console: consoleMessages.map(String),
|
|
480
623
|
actions: actions
|
|
481
624
|
.filter((a) => typeof a === 'object' && a !== null)
|
|
@@ -536,6 +679,26 @@ function requestLine(request) {
|
|
|
536
679
|
: '';
|
|
537
680
|
return `${head}${sent}${got}`;
|
|
538
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* One request as the note writes it when the body matters as much as the status: the app's own
|
|
684
|
+
* answer, printed to `gotChars`.
|
|
685
|
+
*
|
|
686
|
+
* @param request - The request.
|
|
687
|
+
* @param gotChars - How much of the response body to print.
|
|
688
|
+
* @returns The request's clause.
|
|
689
|
+
*/
|
|
690
|
+
function requestWithBody(request, gotChars) {
|
|
691
|
+
const status = request.status > 0
|
|
692
|
+
? String(request.status)
|
|
693
|
+
: request.failure
|
|
694
|
+
? `no response (${bodyLine(redactSecrets(request.failure), TRACE_NOTE_FAILURE_CHARS)})`
|
|
695
|
+
: 'no response';
|
|
696
|
+
const sent = request.sent
|
|
697
|
+
? ` sent ${bodyLine(redactSecrets(request.sent), TRACE_NOTE_SENT_CHARS)}`
|
|
698
|
+
: '';
|
|
699
|
+
const got = request.got ? ` got ${bodyLine(redactSecrets(request.got), gotChars)}` : '';
|
|
700
|
+
return `${requestLabel(request)} → ${status}${sent}${got}`;
|
|
701
|
+
}
|
|
539
702
|
/**
|
|
540
703
|
* The failing step's own request as the note writes it first: `the failing step sent DELETE
|
|
541
704
|
* /api/profiles/x/follow → 200 got {"profile":{…}}`.
|
|
@@ -548,18 +711,27 @@ function requestLine(request) {
|
|
|
548
711
|
* @returns The clause the test's row opens with.
|
|
549
712
|
*/
|
|
550
713
|
function leadRequestLine(request) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
714
|
+
return `the failing step sent ${requestWithBody(request, TRACE_NOTE_LEAD_GOT_CHARS)}`;
|
|
715
|
+
}
|
|
716
|
+
/** A request's identity in a trace: method, path and status. */
|
|
717
|
+
function requestKey(request) {
|
|
718
|
+
return `${request.method} ${request.path} ${request.status}`;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* When an action ran, relative to the first action the trace recorded — `+2.9 s`.
|
|
722
|
+
*
|
|
723
|
+
* The trace's clock starts with the worker process, so the raw value is meaningless to a reader;
|
|
724
|
+
* a relative time keeps the trace's own ordering visible without inventing a wall-clock time.
|
|
725
|
+
*
|
|
726
|
+
* @param item - The trace.
|
|
727
|
+
* @param start - The action's start, on the trace's clock.
|
|
728
|
+
* @returns The time to print.
|
|
729
|
+
*/
|
|
730
|
+
function actionClock(item, start) {
|
|
731
|
+
const first = item.actions?.[0]?.start;
|
|
732
|
+
if (first === undefined || !Number.isFinite(first) || start < first)
|
|
733
|
+
return `${Math.round(start)} ms`;
|
|
734
|
+
return `+${((start - first) / 1000).toFixed(1)} s`;
|
|
563
735
|
}
|
|
564
736
|
/** Whether a sent body is an object whose only top-level key is `data` — the wrapped-body trap. */
|
|
565
737
|
function wrappedInData(text) {
|
|
@@ -589,6 +761,16 @@ function wrappedInData(text) {
|
|
|
589
761
|
* invisible to the next proof run. The note is about the test's own API calls.
|
|
590
762
|
*/
|
|
591
763
|
const STATIC_ASSET_PATH = /^\/(?:sw|service-worker)\.js(?:[?#]|$)|\/manifest\.webmanifest(?:[?#]|$)|\/favicon[^/]*(?:[?#]|$)|\/fonts?\//i;
|
|
764
|
+
/**
|
|
765
|
+
* The PWA worker's own hashed script, the manifest's touch icons and the splash images.
|
|
766
|
+
*
|
|
767
|
+
* The worker and the shell type these `other`/`fetch`, not `script`/`image`, so
|
|
768
|
+
* {@link STATIC_ASSET_TYPES} alone never caught them: on xr-molecule-flash-m148 every failing
|
|
769
|
+
* test's row filled three of its four request slots with `GET /workbox-d8ed3718.js → 200` and
|
|
770
|
+
* `GET /icons/splash-1125x2436.svg → 200`, and the register `POST /api/users` that explains the
|
|
771
|
+
* signed-out page never appeared.
|
|
772
|
+
*/
|
|
773
|
+
const SHELL_FILE_PATH = /^\/workbox-[^/]*\.js(?:[?#]|$)|^\/icons\/|^\/(?:apple-touch-icon|og-image|splash)[^/]*(?:[?#]|$)/i;
|
|
592
774
|
/** The build/dev output paths the page shell loads (Vite's bundle, the platform's own files). */
|
|
593
775
|
const STATIC_ASSET_DIR = /^\/(?:assets|@vite|__mol)\//i;
|
|
594
776
|
/** The resource types a page loads for itself rather than for the step under test. */
|
|
@@ -620,7 +802,7 @@ export function isStaticAssetRequest(request) {
|
|
|
620
802
|
if (request.type !== undefined && STATIC_ASSET_TYPES.has(request.type))
|
|
621
803
|
return true;
|
|
622
804
|
const path = requestPath(request);
|
|
623
|
-
return STATIC_ASSET_PATH.test(path) || STATIC_ASSET_DIR.test(path);
|
|
805
|
+
return STATIC_ASSET_PATH.test(path) || STATIC_ASSET_DIR.test(path) || SHELL_FILE_PATH.test(path);
|
|
624
806
|
}
|
|
625
807
|
/**
|
|
626
808
|
* Whether a trace recorded a request the server actually failed: a 4xx/5xx, or a network error.
|
|
@@ -918,11 +1100,16 @@ function missingApiPrefixSentence(request) {
|
|
|
918
1100
|
* line rather than inferred from the status; when one of those bodies is an object whose only key
|
|
919
1101
|
* is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
|
|
920
1102
|
*
|
|
921
|
-
* Every row opens with the
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
1103
|
+
* Every row opens with the test's LAST user action before the failing assertion and the requests
|
|
1104
|
+
* that action caused ({@link TraceArtifact.lastAction}) — started after it, before the assertion,
|
|
1105
|
+
* the app's own API calls first, each with its status and the first 200 characters of its response,
|
|
1106
|
+
* then where the page went next. The trace's action clock and each request's `_monotonicTime` are
|
|
1107
|
+
* the same clock, so "caused" is the trace's own fact rather than a guess from recording order.
|
|
1108
|
+
* xr-molecule-flash-m148 failed 13 then 12 tests on one unnamed cause while every row led with
|
|
1109
|
+
* `GET /api/tags → 200` — the Home page's own fetch after the redirect — and filled its answered
|
|
1110
|
+
* slots with the PWA's `/workbox-*.js` and its `/icons/*.svg`; the register `POST /api/users`
|
|
1111
|
+
* (answered 201, the page still rendering signed-out) never appeared. With no action clock, the
|
|
1112
|
+
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
926
1113
|
*
|
|
927
1114
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
928
1115
|
* needs a recorded response, a real failure or a console error ({@link traceRowEvidence}), rows are
|
|
@@ -1009,17 +1196,20 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1009
1196
|
];
|
|
1010
1197
|
const rows = [];
|
|
1011
1198
|
for (const [index, item] of traced.entries()) {
|
|
1012
|
-
// The
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1199
|
+
// The test's LAST action before the failing assertion leads the row (m148): the requests it
|
|
1200
|
+
// caused and what came back explain a page that never showed the signed-in state, while the
|
|
1201
|
+
// page's own last call (a redirect target's `GET /api/tags`) and the shell's files do not.
|
|
1015
1202
|
const lead = item.lead;
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1203
|
+
const step = item.lastAction;
|
|
1204
|
+
// A navigation that never finished has one explanation — the stalled asset it was waiting on
|
|
1205
|
+
// (m135) — and that leads the row instead of the action's own document request.
|
|
1206
|
+
const stalledNav = navigationNeverFinished(item) && item.requests.some(stalledDocumentSubresource);
|
|
1207
|
+
const stepRequests = step !== undefined && !stalledNav ? step.requests : [];
|
|
1208
|
+
const stepKeys = new Set(stepRequests.map(requestKey));
|
|
1209
|
+
const isLead = (request) => lead !== undefined && requestKey(request) === requestKey(lead);
|
|
1020
1210
|
// The page shell's own requests take no row, in any bucket: an aborted `/sw.js` would
|
|
1021
1211
|
// otherwise be printed as an "answered" request with no response (m125 +29:33).
|
|
1022
|
-
const requests = item.requests.filter((request) => !isStaticAssetRequest(request) && !isLead(request));
|
|
1212
|
+
const requests = item.requests.filter((request) => !isStaticAssetRequest(request) && !isLead(request) && !stepKeys.has(requestKey(request)));
|
|
1023
1213
|
const failed = requests.filter(failedRequest);
|
|
1024
1214
|
const unanswered = requests.filter(unansweredRequest);
|
|
1025
1215
|
const answered = requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
|
|
@@ -1030,7 +1220,27 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1030
1220
|
: [];
|
|
1031
1221
|
let room = MAX_TRACE_NOTE_REQUESTS;
|
|
1032
1222
|
const parts = [];
|
|
1033
|
-
if (
|
|
1223
|
+
if (step !== undefined && !stalledNav) {
|
|
1224
|
+
const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
|
|
1225
|
+
if (step.requests.length === 0) {
|
|
1226
|
+
parts.push(`${head}, and it sent no request`);
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
const shown = step.requests.slice(0, room);
|
|
1230
|
+
room -= shown.length;
|
|
1231
|
+
// The response body is the point (m148: the register `POST /api/users` answered 201 and
|
|
1232
|
+
// the page rendered signed-out anyway) — printed whatever the status.
|
|
1233
|
+
parts.push(`${head}, and it sent ${shown.map((request) => requestWithBody(request, TRACE_NOTE_LEAD_GOT_CHARS)).join('; ')}`);
|
|
1234
|
+
}
|
|
1235
|
+
// What the page did next: a navigation shows in the main frame's URL even when the app moved
|
|
1236
|
+
// client-side and the network log recorded no document request.
|
|
1237
|
+
if (step.pageAfter !== undefined) {
|
|
1238
|
+
parts.push(step.pageBefore !== undefined && step.pageBefore !== step.pageAfter
|
|
1239
|
+
? `the page then navigated to ${step.pageAfter}`
|
|
1240
|
+
: `the page stayed at ${step.pageAfter}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else if (lead !== undefined) {
|
|
1034
1244
|
parts.push(leadRequestLine(lead));
|
|
1035
1245
|
room -= 1;
|
|
1036
1246
|
}
|
|
@@ -1052,7 +1262,12 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1052
1262
|
shown.map(requestLabel).join('; '));
|
|
1053
1263
|
}
|
|
1054
1264
|
if (answered.length > 0 && room > 0) {
|
|
1055
|
-
|
|
1265
|
+
// An action that sent nothing leaves the page's own load as the only requests there are:
|
|
1266
|
+
// name them for what they are, not as the action's answer.
|
|
1267
|
+
const label = step !== undefined && !stalledNav && step.requests.length === 0
|
|
1268
|
+
? 'also answered (started before the action)'
|
|
1269
|
+
: 'answered';
|
|
1270
|
+
parts.push(`${label}: ${answered.slice(0, room).map(requestLine).join('; ')}`);
|
|
1056
1271
|
}
|
|
1057
1272
|
if (item.console.length > 0)
|
|
1058
1273
|
parts.push(`console: ${item.console
|
|
@@ -1104,8 +1319,9 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1104
1319
|
return (transportLead +
|
|
1105
1320
|
stalledLead +
|
|
1106
1321
|
`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
|
|
1322
|
+
`(what each page sent and got — first what the test's last action sent and where the page went ` +
|
|
1323
|
+
`next, then the requests that failed, the ones that got no response, and its last answered ` +
|
|
1324
|
+
`ones):\n` +
|
|
1109
1325
|
`${rows.join('\n')}${more}\n` +
|
|
1110
1326
|
(anyFailed
|
|
1111
1327
|
? everyFailureLacksPrefix
|
|
@@ -1119,10 +1335,12 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1119
1335
|
'page-state bug: re-run these tests against the same URL first, and change the app only ' +
|
|
1120
1336
|
'if the same navigation times out again with no stalled asset in the trace.'
|
|
1121
1337
|
: 'No listed request failed — none got a 4xx/5xx or a network error — so the cause is in the ' +
|
|
1122
|
-
|
|
1123
|
-
'row)
|
|
1124
|
-
'
|
|
1125
|
-
'
|
|
1338
|
+
"page, not the API. Start with what the test's last action sent (the first line of each " +
|
|
1339
|
+
'row) and what the page did next: a request that answered (2xx included) while the page ' +
|
|
1340
|
+
'still did not show the state the assertion waited on is the fact to read — its status ' +
|
|
1341
|
+
'and response body, not the selector. If the request is not the one the test meant — an ' +
|
|
1342
|
+
'unfollow sent where the test clicked follow — the state the action read was stale ' +
|
|
1343
|
+
'before it.') +
|
|
1126
1344
|
prefixAdvice +
|
|
1127
1345
|
(wrapped
|
|
1128
1346
|
? ' The API received a body wrapped in `data`: app-http’s `post(url, data, config)` / ' +
|