@molecule/api-ai-tools 1.9.12 → 1.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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-18T07:36:48.788Z
6
+ Generated: 2026-10-18T09:17:15.122Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -522,6 +522,52 @@ interface TraceArtifact {
522
522
  }
523
523
  ```
524
524
 
525
+ #### `TraceRunFailure`
526
+
527
+ What the runner's own report says about ONE failing test: where the test is declared, which
528
+ Playwright call its failure names, and the per-test deadline the failure measured against.
529
+
530
+ A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
531
+ xr-molecule-flash-m153 a 15 s `page.waitForSelector` timeout left a trace whose `Frame.goto`
532
+ never finished (the test was cut off mid-navigation) with a `/src/components/*.tsx` module still
533
+ pending when the context closed — and the note read that as an asset hang, then sent the run to
534
+ re-run the same tests unchanged against the same dev server while the same result's
535
+ `testRunNote` was telling it to measure a production build.
536
+
537
+ ```typescript
538
+ interface TraceRunFailure {
539
+ /** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
540
+ spec: string
541
+ /** The line the test is DECLARED on, from that heading (`206`). */
542
+ line: number
543
+ /** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
544
+ operation: string | null
545
+ /** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
546
+ budgetMs: number | null
547
+ }
548
+ ```
549
+
550
+ #### `TraceRunReport`
551
+
552
+ The runner's own report, beside the traces it wrote.
553
+
554
+ ```typescript
555
+ interface TraceRunReport {
556
+ /**
557
+ * One entry per failing test, as the runner printed them. Empty when the caller supplied none:
558
+ * without the runner's reported operation a pending asset is never called the cause.
559
+ */
560
+ failures?: readonly TraceRunFailure[]
561
+ /**
562
+ * True when the same result's `testRunNote` already names a production build as the next
563
+ * measurement (a local dev-server walk that ran out of its per-test budget). The note then
564
+ * states the ONE next step instead of directing an unchanged same-target re-run that
565
+ * contradicts it.
566
+ */
567
+ productionBuildAdvised?: boolean
568
+ }
569
+ ```
570
+
525
571
  #### `DiagnosisTraceRewrite`
526
572
 
527
573
  A command with the trace flag this platform decided to add, and the sentence to report it in.
@@ -1953,7 +1999,7 @@ function stalledDocumentSubresource(request: TraceRequest): boolean
1953
1999
 
1954
2000
  **Returns:** True when the request is a document subresource that never finished.
1955
2001
 
1956
- #### `failureTraceNote(items, target, earlierFailures, apiPrefixes)`
2002
+ #### `failureTraceNote(items, target, earlierFailures, apiPrefixes, report)`
1957
2003
 
1958
2004
  The note a failed suite run's result carries: what each failing test's page actually sent, and
1959
2005
  what came back.
@@ -1985,6 +2031,7 @@ function failureTraceNote(
1985
2031
  target: string,
1986
2032
  earlierFailures?: readonly string[],
1987
2033
  apiPrefixes?: readonly string[],
2034
+ report?: TraceRunReport,
1988
2035
  ): string | null
1989
2036
  ```
1990
2037
 
@@ -1992,6 +2039,7 @@ function failureTraceNote(
1992
2039
  - `target` — Where the run went — the deployed host, as the note names it.
1993
2040
  - `earlierFailures` — The failing `file:line`s earlier walks of this suite left, so a repeat is ranked above a one-off. Empty when the caller knows of no earlier walk.
1994
2041
  - `apiPrefixes` — The API path prefixes the app's client config names (`['/api']`), read from the app by the caller. Empty when the caller could not read one; the traces' own requests may still supply it. Never a hard-coded literal.
2042
+ - `report` — The runner's own report for this run ({@link TraceRunReport}): each failing test's declared location, the operation its failure names and the deadline it names, plus whether the same result's `testRunNote` already points at a production build. A pending asset is called the cause only when the runner says the failing operation for THAT test is the navigation it was loading; without the report nothing is called a navigation stall.
1995
2043
 
1996
2044
  **Returns:** The note, or null when no trace held a recorded response, a real failure or a console error.
1997
2045
 
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAkOD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AA2GD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAu/ExF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAkOD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AA2GD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAokFxF"}
package/dist/tools.js CHANGED
@@ -849,6 +849,65 @@ export function buildTools(backend, config) {
849
849
  result = isEnvFilePath(path) ? redactSecrets(result) : redactSecretsInCode(result);
850
850
  return result;
851
851
  }
852
+ // ── Binary reads ───────────────────────────────────────────────
853
+ /** Extensions whose files are never text: a `read_file` on one is refused by name. */
854
+ const BINARY_FILE_EXTENSIONS = new Set([
855
+ 'avif',
856
+ 'gif',
857
+ 'gz',
858
+ 'ico',
859
+ 'jpeg',
860
+ 'jpg',
861
+ 'otf',
862
+ 'pdf',
863
+ 'png',
864
+ 'tgz',
865
+ 'ttf',
866
+ 'wasm',
867
+ 'webp',
868
+ 'woff',
869
+ 'woff2',
870
+ 'zip',
871
+ ]);
872
+ /** How many chars of a read are inspected for binary markers. */
873
+ const BINARY_SAMPLE_CHARS = 512;
874
+ /**
875
+ * A replacement character (a byte UTF-8 could not decode) marks binary only in
876
+ * bulk: one literal U+FFFD in a source string is ordinary text.
877
+ */
878
+ const BINARY_MIN_REPLACEMENTS = 8;
879
+ const BINARY_REPLACEMENT_DENSITY = 0.05;
880
+ /**
881
+ * The kind of a BINARY file, or `null` for text.
882
+ *
883
+ * `read_file` asks the backend for a string, so a PNG comes back as UTF-8
884
+ * replacement characters and used to be handed to the model whole — 21,191
885
+ * chars of unreadable bytes it cannot look at, ~12k tokens per read, asked for
886
+ * twice in one run (xr-molecule-flash-m155 +09:58). A known binary extension is
887
+ * refused by name; a file without one is caught from the bytes themselves, by
888
+ * a NUL byte or by the density of replacement characters in its first
889
+ * {@link BINARY_SAMPLE_CHARS} chars.
890
+ *
891
+ * @param path - The already-resolved path.
892
+ * @param content - The content the backend decoded as UTF-8.
893
+ * @returns The binary kind to name (e.g. `png`, `binary data`), or `null` for text.
894
+ */
895
+ function binaryFileKind(path, content) {
896
+ const ext = posix.extname(path).slice(1).toLowerCase();
897
+ if (BINARY_FILE_EXTENSIONS.has(ext))
898
+ return ext;
899
+ const sample = content.slice(0, BINARY_SAMPLE_CHARS);
900
+ if (sample.includes('\u0000'))
901
+ return 'binary data';
902
+ let replacements = 0;
903
+ for (const char of sample)
904
+ if (char === '\uFFFD')
905
+ replacements += 1;
906
+ return replacements >= BINARY_MIN_REPLACEMENTS &&
907
+ replacements / sample.length >= BINARY_REPLACEMENT_DENSITY
908
+ ? 'binary data'
909
+ : null;
910
+ }
852
911
  // ── Empty-read verification ────────────────────────────────────
853
912
  /**
854
913
  * The file's size in bytes as the backend's own shell reports it, or `null`
@@ -1022,6 +1081,19 @@ export function buildTools(backend, config) {
1022
1081
  ...(verdict.note ? { note: verdict.note } : {}),
1023
1082
  });
1024
1083
  }
1084
+ // A binary file decoded as UTF-8 is unreadable by definition — say what it
1085
+ // is instead of returning the bytes (see binaryFileKind).
1086
+ const binaryKind = binaryFileKind(path, content);
1087
+ if (binaryKind) {
1088
+ // The read already happened, so the decoded length is the fallback size;
1089
+ // `wc -c` gives the file's real byte count when the backend can run it.
1090
+ const bytes = (await probeFileSize(path)) ?? content.length;
1091
+ return {
1092
+ error: `${path} is a binary file (${binaryKind}, ${Math.max(1, Math.round(bytes / 1024))} KB). ` +
1093
+ `read_file returns text only — to look at a page use screenshot_preview; to inspect an ` +
1094
+ `image you produced, measure it in a script and print the numbers (its size, pixel counts).`,
1095
+ };
1096
+ }
1025
1097
  if (content.length > MAX_READ_SIZE)
1026
1098
  return {
1027
1099
  error: `File too large (${Math.round(content.length / 1024)}KB). Maximum is ${MAX_READ_SIZE / 1024 / 1024}MB.`,
@@ -2911,8 +2983,13 @@ export function buildTools(backend, config) {
2911
2983
  : { piped: false, unplaced: false };
2912
2984
  const reported = suiteRunExitCode(exitCode, output, placement.piped);
2913
2985
  const noCodeNote = suiteExitCodeNote(placement, reported);
2986
+ // A FINISHED result says so, even when the code it would report is withheld: a filtered
2987
+ // pipeline's code is not the run's verdict ({@link FILTERED_EXIT_NOTE}), so `exitCode` alone
2988
+ // cannot tell a caller this is the delivered result rather than a progress return — and a
2989
+ // caller that tracks background tasks would hand the same result back as fresh news later.
2914
2990
  return {
2915
2991
  taskId,
2992
+ status: 'completed',
2916
2993
  exitCode: reported,
2917
2994
  waitedMs,
2918
2995
  ...(causes || noCodeNote
@@ -279,6 +279,42 @@ export declare function unansweredRequest(request: TraceRequest): boolean;
279
279
  * @returns True when the request is a document subresource that never finished.
280
280
  */
281
281
  export declare function stalledDocumentSubresource(request: TraceRequest): boolean;
282
+ /**
283
+ * What the runner's own report says about ONE failing test: where the test is declared, which
284
+ * Playwright call its failure names, and the per-test deadline the failure measured against.
285
+ *
286
+ * A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
287
+ * xr-molecule-flash-m153 a 15 s `page.waitForSelector` timeout left a trace whose `Frame.goto`
288
+ * never finished (the test was cut off mid-navigation) with a `/src/components/*.tsx` module still
289
+ * pending when the context closed — and the note read that as an asset hang, then sent the run to
290
+ * re-run the same tests unchanged against the same dev server while the same result's
291
+ * `testRunNote` was telling it to measure a production build.
292
+ */
293
+ export interface TraceRunFailure {
294
+ /** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
295
+ spec: string;
296
+ /** The line the test is DECLARED on, from that heading (`206`). */
297
+ line: number;
298
+ /** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
299
+ operation: string | null;
300
+ /** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
301
+ budgetMs: number | null;
302
+ }
303
+ /** The runner's own report, beside the traces it wrote. */
304
+ export interface TraceRunReport {
305
+ /**
306
+ * One entry per failing test, as the runner printed them. Empty when the caller supplied none:
307
+ * without the runner's reported operation a pending asset is never called the cause.
308
+ */
309
+ failures?: readonly TraceRunFailure[];
310
+ /**
311
+ * True when the same result's `testRunNote` already names a production build as the next
312
+ * measurement (a local dev-server walk that ran out of its per-test budget). The note then
313
+ * states the ONE next step instead of directing an unchanged same-target re-run that
314
+ * contradicts it.
315
+ */
316
+ productionBuildAdvised?: boolean;
317
+ }
282
318
  /**
283
319
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
284
320
  * what came back.
@@ -321,12 +357,16 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
321
357
  * transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
322
358
  * rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
323
359
  *
324
- * When a navigation never finished and one of the document's own subresources (a stylesheet, a
325
- * script, an asset under `/assets/`) never got a response either, that request takes a row and the
326
- * note says the page was waiting on it. It is not an API call, but on a `page.goto` timeout it is
327
- * the only request that can explain the timeout — the deployed app was serving the page while one
328
- * fetch stalled for 27 s (xr-molecule-flash-m135 +34:10). The page shell's incidental requests
329
- * (`/sw.js`, the manifest, a favicon, a font) stay out, for m125's reason.
360
+ * When a navigation the runner reports as the failure never finished and one of the document's
361
+ * own subresources (a stylesheet, a script, an asset under `/assets/`) never got a response either
362
+ * — having started inside that wait — the request takes a row and the note says the page was
363
+ * waiting on it. It is not an API call, but on a `page.goto` timeout it is the only request that
364
+ * can explain the timeout — the deployed app was serving the page while one fetch stalled for 27 s
365
+ * (xr-molecule-flash-m135 +34:10). The page shell's incidental requests (`/sw.js`, the manifest, a
366
+ * favicon, a font) stay out, for m125's reason. An asset merely unfinished when a selector wait
367
+ * ran out the test budget, or one a teardown/`afterEach` trace holds, is not a cause: the note
368
+ * says it "had no recorded response when the test ended; that alone does not establish the cause"
369
+ * (xr-molecule-flash-m153 +13:43).
330
370
  *
331
371
  * A 404 whose path lacks the API prefix the app's own calls carry is called out with that prefix
332
372
  * ({@link missingApiPrefixRequests}): xr-molecule-flash-m145 posted `/users` to `vite preview`,
@@ -351,10 +391,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
351
391
  * @param apiPrefixes - The API path prefixes the app's client config names (`['/api']`), read from
352
392
  * the app by the caller. Empty when the caller could not read one; the traces' own requests may
353
393
  * still supply it. Never a hard-coded literal.
394
+ * @param report - The runner's own report for this run ({@link TraceRunReport}): each failing
395
+ * test's declared location, the operation its failure names and the deadline it names, plus
396
+ * whether the same result's `testRunNote` already points at a production build. A pending asset
397
+ * is called the cause only when the runner says the failing operation for THAT test is the
398
+ * navigation it was loading; without the report nothing is called a navigation stall.
354
399
  * @returns The note, or null when no trace held a recorded response, a real failure or a console
355
400
  * error.
356
401
  */
357
- export declare function failureTraceNote(items: readonly TraceArtifact[], target: string, earlierFailures?: readonly string[], apiPrefixes?: readonly string[]): string | null;
402
+ export declare function failureTraceNote(items: readonly TraceArtifact[], target: string, earlierFailures?: readonly string[], apiPrefixes?: readonly string[], report?: TraceRunReport): string | null;
358
403
  /**
359
404
  * Whether a Playwright config records traces for the tests that fail.
360
405
  *
@@ -1 +1 @@
1
- {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,6FAA6F;IAC7F,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B;;;;OAIG;IACH,OAAO,EAAE,WAAW,EAAE,CAAA;CACvB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,iDAAiD;AACjD,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAO,MAigBvC,CAAA;AAoDD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAkCnE;AA2KD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAKnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAG5D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAGhE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAMzE;AAkDD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IACrC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AA4ZD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,SAAS,MAAM,EAAO,EACvC,WAAW,GAAE,SAAS,MAAM,EAAO,EACnC,MAAM,GAAE,cAAmB,GAC1B,MAAM,GAAG,IAAI,CA0Rf;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,8BAA8B,CAAA;AAwG/D,oGAAoG;AACpG,MAAM,WAAW,qBAAqB;IACpC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5C,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA2BjC"}
@@ -452,6 +452,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
452
452
  ...(r.sent ? { sent: r.sent } : {}),
453
453
  ...(r.got ? { got: r.got } : {}),
454
454
  type: r.type || undefined,
455
+ // The trace's own clock, so the note can tell a request the navigation was waiting on from
456
+ // one that was already pending before it started (m135/m153).
457
+ ...(typeof r.startedAt === 'number' ? { startedAt: r.startedAt } : {}),
455
458
  ...(r.sentAuthorization ? { sentAuthorization: true } : {}),
456
459
  ...(r.authorization ? { authorization: r.authorization } : {}),
457
460
  ...(r.sentCookie ? { sentCookie: true } : {}),
@@ -965,6 +968,97 @@ function repeatsEarlierFailure(item, earlier) {
965
968
  }
966
969
  return false;
967
970
  }
971
+ /** The Playwright navigation calls whose wait a pending document subresource can explain. */
972
+ const NAVIGATION_OPERATIONS = new Set(['goto', 'waitForURL', 'waitForLoadState', 'reload']);
973
+ /**
974
+ * The method a runner-reported operation names (`page.waitForSelector` → `waitForSelector`), or
975
+ * null when it is not one of the navigation calls a stalled asset can explain.
976
+ *
977
+ * @param operation - The call the runner's error text names, or null.
978
+ * @returns The navigation method, or null.
979
+ */
980
+ function navigationMethod(operation) {
981
+ if (operation === null)
982
+ return null;
983
+ const method = operation.slice(operation.lastIndexOf('.') + 1);
984
+ return NAVIGATION_OPERATIONS.has(method) ? method : null;
985
+ }
986
+ /**
987
+ * The runner's report for the test THIS trace belongs to, or undefined when it belongs to none.
988
+ *
989
+ * The trace names the line of its LAST recorded step; the runner's summary names the test's
990
+ * DECLARATION line, and the step sits inside the body just after it ({@link
991
+ * RECURRENCE_LINE_WINDOW}). A line BEFORE the declaration is a hook — m153's teardown trace sits
992
+ * at `url-navigation.spec.ts:107`, the Pagination suite's `afterEach`, while the failing test is
993
+ * declared at `:206` — and a hook is not the failing test's operation.
994
+ *
995
+ * @param item - One traced failure.
996
+ * @param failures - The runner's report, one entry per failing test.
997
+ * @returns The matching failure, or undefined.
998
+ */
999
+ function runFailureOf(item, failures) {
1000
+ if (item.spec === null)
1001
+ return undefined;
1002
+ for (const failure of failures) {
1003
+ if (!sameLocationFile(item.spec, failure.spec))
1004
+ continue;
1005
+ if (item.line < failure.line)
1006
+ continue;
1007
+ if (item.line - failure.line <= RECURRENCE_LINE_WINDOW)
1008
+ return failure;
1009
+ }
1010
+ return undefined;
1011
+ }
1012
+ /**
1013
+ * Whether a pending request's interval covers a navigation's wait: it started while the
1014
+ * navigation was loading (at or after the wait began) and it has no recorded response, so it was
1015
+ * still pending when the wait ran out. A request that started before the wait belongs to the page
1016
+ * already on screen, not to the navigation that never finished.
1017
+ *
1018
+ * @param request - One request the trace recorded.
1019
+ * @param wait - The navigation action the runner reported.
1020
+ * @returns True when the request's pending interval covers the wait.
1021
+ */
1022
+ function requestCoversWait(request, wait) {
1023
+ if (typeof request.startedAt !== 'number')
1024
+ return false;
1025
+ return request.startedAt >= wait.start - 1;
1026
+ }
1027
+ /**
1028
+ * The stalled-navigation reading for one trace — or null.
1029
+ *
1030
+ * All three facts must hold together: the runner says THIS test's failing operation is a
1031
+ * navigation ({@link NAVIGATION_OPERATIONS}); the trace's record of that call never finished; and
1032
+ * a document subresource still pending at the end started inside that wait
1033
+ * ({@link requestCoversWait}). An asset merely unfinished when the test was cancelled, or one a
1034
+ * teardown/`afterEach` trace holds, meets none of these and is not a cause (m153).
1035
+ *
1036
+ * @param item - One traced failure.
1037
+ * @param report - The runner's report and the run's next step.
1038
+ * @returns The wait and the assets it was waiting on, or null.
1039
+ */
1040
+ function stalledNavigation(item, report) {
1041
+ const failure = runFailureOf(item, report.failures ?? []);
1042
+ if (failure === undefined)
1043
+ return null;
1044
+ const method = navigationMethod(failure.operation);
1045
+ if (method === null)
1046
+ return null;
1047
+ let wait = null;
1048
+ for (const action of item.actions) {
1049
+ if (!action.api.endsWith(`.${method}`))
1050
+ continue;
1051
+ if (wait === null || action.start > wait.start)
1052
+ wait = action;
1053
+ }
1054
+ // The navigation is the failure only while it is still running at the end of the test.
1055
+ if (wait === null || wait.end !== null)
1056
+ return null;
1057
+ const assets = item.requests.filter((request) => stalledDocumentSubresource(request) && requestCoversWait(request, wait));
1058
+ if (assets.length === 0)
1059
+ return null;
1060
+ return { reported: failure.operation ?? wait.api, wait, assets, budgetMs: failure.budgetMs };
1061
+ }
968
1062
  /**
969
1063
  * Whether a trace holds evidence the note can explain: a request that got a RECORDED response, a
970
1064
  * real failure (4xx/5xx or a network error), or a console line. A request with no response is
@@ -974,31 +1068,38 @@ function repeatsEarlierFailure(item, earlier) {
974
1068
  *
975
1069
  * A request the page shell made (m125 +29:33: `/sw.js`, a font) is not evidence about the step
976
1070
  * either, answered or not. The exception is a document subresource that never finished while the
977
- * page's own navigation was still open: the navigation waits on it, so it is the reason the
978
- * `page.goto` timed out ({@link stalledDocumentSubresource}).
1071
+ * navigation the RUNNER reports failing was still open: that navigation waits on it, so it is the
1072
+ * reason the `page.goto` timed out ({@link stalledNavigation}).
979
1073
  *
980
1074
  * @param item - One traced failure.
1075
+ * @param report - The runner's report and the run's next step.
981
1076
  * @returns True when the trace holds a request or console line worth a row.
982
1077
  */
983
- function traceRowEvidence(item) {
1078
+ function traceRowEvidence(item, report) {
984
1079
  return (item.console.length > 0 ||
985
1080
  item.requests.some((request) => !isStaticAssetRequest(request) && !unansweredRequest(request)) ||
986
- // The one shape in which a subresource with no response IS the cause: the navigation it was
987
- // part of never finished (see {@link stalledDocumentSubresource}).
988
- (navigationNeverFinished(item) && item.requests.some(stalledDocumentSubresource)));
1081
+ // The one shape in which a subresource with no response IS the cause: the navigation the
1082
+ // runner reported was still running at the end (see {@link stalledNavigation}).
1083
+ stalledNavigation(item, report) !== null);
989
1084
  }
990
1085
  /**
991
1086
  * The note's row order: a row with a REAL failure first, then a row whose failure an earlier walk
992
1087
  * also had, then the rest. A recurrence is the only evidence a row carries when its requests were
993
1088
  * all answered (m112's favorite cause: the click's own request answered, the button never
994
1089
  * changed).
1090
+ *
1091
+ * @param item - One traced failure.
1092
+ * @param earlier - The failing `file:line`s earlier walks of this suite left.
1093
+ * @param report - The runner's report and the run's next step.
1094
+ * @returns The row's tier: 0 first, 2 last.
995
1095
  */
996
- function traceRowTier(item, earlier) {
1096
+ function traceRowTier(item, earlier, report) {
997
1097
  if (item.requests.some(failedRequest))
998
1098
  return 0;
999
- // A navigation that never finished because its own subresource never got a response is as
1000
- // strong a cause as a failed request — it is the one fact the timeout was waiting on (m135).
1001
- if (navigationNeverFinished(item) && item.requests.some(stalledDocumentSubresource))
1099
+ // A navigation the runner reported, still waiting on a subresource that never got a response,
1100
+ // is as strong a cause as a failed request — it is the one fact the timeout was waiting on
1101
+ // (m135).
1102
+ if (stalledNavigation(item, report) !== null)
1002
1103
  return 0;
1003
1104
  if (repeatsEarlierFailure(item, earlier))
1004
1105
  return 1;
@@ -1267,12 +1368,16 @@ function missingApiPrefixSentence(request) {
1267
1368
  * transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
1268
1369
  * rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
1269
1370
  *
1270
- * When a navigation never finished and one of the document's own subresources (a stylesheet, a
1271
- * script, an asset under `/assets/`) never got a response either, that request takes a row and the
1272
- * note says the page was waiting on it. It is not an API call, but on a `page.goto` timeout it is
1273
- * the only request that can explain the timeout — the deployed app was serving the page while one
1274
- * fetch stalled for 27 s (xr-molecule-flash-m135 +34:10). The page shell's incidental requests
1275
- * (`/sw.js`, the manifest, a favicon, a font) stay out, for m125's reason.
1371
+ * When a navigation the runner reports as the failure never finished and one of the document's
1372
+ * own subresources (a stylesheet, a script, an asset under `/assets/`) never got a response either
1373
+ * — having started inside that wait — the request takes a row and the note says the page was
1374
+ * waiting on it. It is not an API call, but on a `page.goto` timeout it is the only request that
1375
+ * can explain the timeout — the deployed app was serving the page while one fetch stalled for 27 s
1376
+ * (xr-molecule-flash-m135 +34:10). The page shell's incidental requests (`/sw.js`, the manifest, a
1377
+ * favicon, a font) stay out, for m125's reason. An asset merely unfinished when a selector wait
1378
+ * ran out the test budget, or one a teardown/`afterEach` trace holds, is not a cause: the note
1379
+ * says it "had no recorded response when the test ended; that alone does not establish the cause"
1380
+ * (xr-molecule-flash-m153 +13:43).
1276
1381
  *
1277
1382
  * A 404 whose path lacks the API prefix the app's own calls carry is called out with that prefix
1278
1383
  * ({@link missingApiPrefixRequests}): xr-molecule-flash-m145 posted `/users` to `vite preview`,
@@ -1297,17 +1402,22 @@ function missingApiPrefixSentence(request) {
1297
1402
  * @param apiPrefixes - The API path prefixes the app's client config names (`['/api']`), read from
1298
1403
  * the app by the caller. Empty when the caller could not read one; the traces' own requests may
1299
1404
  * still supply it. Never a hard-coded literal.
1405
+ * @param report - The runner's own report for this run ({@link TraceRunReport}): each failing
1406
+ * test's declared location, the operation its failure names and the deadline it names, plus
1407
+ * whether the same result's `testRunNote` already points at a production build. A pending asset
1408
+ * is called the cause only when the runner says the failing operation for THAT test is the
1409
+ * navigation it was loading; without the report nothing is called a navigation stall.
1300
1410
  * @returns The note, or null when no trace held a recorded response, a real failure or a console
1301
1411
  * error.
1302
1412
  */
1303
- export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = []) {
1304
- const candidates = items.filter(traceRowEvidence);
1413
+ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = [], report = {}) {
1414
+ const candidates = items.filter((item) => traceRowEvidence(item, report));
1305
1415
  if (candidates.length === 0)
1306
1416
  return null;
1307
1417
  // Ranked, then deduplicated BY LOCATION — in that order, so the row kept for a location is the
1308
1418
  // one carrying the strongest evidence (m112's duplicate `afterEach` rows).
1309
1419
  const ranked = candidates
1310
- .map((item, index) => ({ item, index, tier: traceRowTier(item, earlierFailures) }))
1420
+ .map((item, index) => ({ item, index, tier: traceRowTier(item, earlierFailures, report) }))
1311
1421
  .sort((left, right) => left.tier - right.tier || left.index - right.index);
1312
1422
  const traced = [];
1313
1423
  const seen = new Set();
@@ -1345,14 +1455,20 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1345
1455
  const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null && (request.status === 401 || request.status === 403));
1346
1456
  const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
1347
1457
  const authAdvice = authRefusalAdvice(traced);
1348
- // The document subresources that never finished anywhere in this read — the assets a timed-out
1349
- // navigation was waiting on, when there is no failed request to explain the timeout.
1350
- const stalledAssets = [
1351
- ...new Set(traced
1352
- .filter(navigationNeverFinished)
1353
- .flatMap((item) => item.requests.filter(stalledDocumentSubresource))
1354
- .map(requestLabel)),
1458
+ // The stalled-navigation reading, per traced test: non-null only when the RUNNER says this
1459
+ // test's failing operation is a navigation and a document subresource still pending at the end
1460
+ // started inside that wait ({@link stalledNavigation}).
1461
+ const stalls = traced.map((item) => stalledNavigation(item, report));
1462
+ // "Every failure this run traced…" is only honest when every traced test meets that bar — one
1463
+ // stalled trace beside a selector-wait failure is not the cause of the run (m153).
1464
+ const everyTracedStalls = stalls.every((stall) => stall !== null);
1465
+ const stalledAssets = everyTracedStalls
1466
+ ? [...new Set(stalls.flatMap((stall) => stall?.assets.map(requestLabel) ?? []))]
1467
+ : [];
1468
+ const stalledWaits = [
1469
+ ...new Set(stalls.flatMap((stall) => (stall === null ? [] : [stall.reported]))),
1355
1470
  ];
1471
+ const anyStalled = stalls.some((stall) => stall !== null);
1356
1472
  const rows = [];
1357
1473
  for (const [index, item] of traced.entries()) {
1358
1474
  // The test's LAST action before the failing assertion leads the row (m148): the requests it
@@ -1360,10 +1476,24 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1360
1476
  // page's own last call (a redirect target's `GET /api/tags`) and the shell's files do not.
1361
1477
  const lead = item.lead;
1362
1478
  const step = item.lastAction;
1363
- // A navigation that never finished has one explanation — the stalled asset it was waiting on
1364
- // (m135) — and that leads the row instead of the action's own document request.
1365
- const stalledNav = navigationNeverFinished(item) && item.requests.some(stalledDocumentSubresource);
1366
- const stepRequests = step !== undefined && !stalledNav ? step.requests : [];
1479
+ // A navigation the runner reported, still waiting on a stalled asset (m135), leads the row
1480
+ // instead of the action's own document request.
1481
+ const stall = stalls[index] ?? null;
1482
+ const stalledNav = stall !== null;
1483
+ // The document subresources that never finished inside the navigation the runner reported:
1484
+ // the page was waiting on them when the test's budget ran out (m135).
1485
+ const stalled = stall?.assets ?? [];
1486
+ // The same kind of pending asset on a trace the runner does NOT report as a navigation
1487
+ // failure (a selector wait that ran out the test budget, an `afterEach`/teardown trace): it is
1488
+ // not a cause, and it is still worth naming so the executor does not have to guess (m153).
1489
+ const stalledKeys = new Set(stalled.map(requestKey));
1490
+ const pendingAssets = item.requests.filter((request) => stalledDocumentSubresource(request) && !stalledKeys.has(requestKey(request)));
1491
+ const pendingKeys = new Set(pendingAssets.map(requestKey));
1492
+ // A pending asset says nothing about what the action got back, so it never rides the action
1493
+ // lead — the honest bucket below names it (m153).
1494
+ const stepRequests = step !== undefined && !stalledNav
1495
+ ? step.requests.filter((request) => !pendingKeys.has(requestKey(request)))
1496
+ : [];
1367
1497
  const stepKeys = new Set(stepRequests.map(requestKey));
1368
1498
  const isLead = (request) => lead !== undefined && requestKey(request) === requestKey(lead);
1369
1499
  // The page shell's own requests take no row, in any bucket: an aborted `/sw.js` would
@@ -1372,20 +1502,16 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1372
1502
  const failed = requests.filter(failedRequest);
1373
1503
  const unanswered = requests.filter(unansweredRequest);
1374
1504
  const answered = requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
1375
- // A document subresource that never finished, on a trace whose navigation never finished:
1376
- // the page was waiting on it when the test's budget ran out (m135).
1377
- const stalled = navigationNeverFinished(item)
1378
- ? item.requests.filter(stalledDocumentSubresource)
1379
- : [];
1505
+ const budget = stall?.budgetMs ?? null;
1380
1506
  let room = MAX_TRACE_NOTE_REQUESTS;
1381
1507
  const parts = [];
1382
1508
  if (step !== undefined && !stalledNav) {
1383
1509
  const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
1384
- if (step.requests.length === 0) {
1510
+ if (stepRequests.length === 0) {
1385
1511
  parts.push(`${head}, and it sent no request`);
1386
1512
  }
1387
1513
  else {
1388
- const shown = step.requests.slice(0, room);
1514
+ const shown = stepRequests.slice(0, room);
1389
1515
  room -= shown.length;
1390
1516
  // The response body is the point (m148: the register `POST /api/users` answered 201 and
1391
1517
  // the page rendered signed-out anyway) — printed whatever the status.
@@ -1412,18 +1538,25 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1412
1538
  const shown = stalled.slice(0, room);
1413
1539
  room -= shown.length;
1414
1540
  parts.push(`never finished: ${shown.map(requestLabel).join('; ')} — a document subresource with no ` +
1415
- 'response, still in flight when the navigation timed out');
1541
+ `response, still in flight when the ${stall?.reported ?? 'navigation'} wait ran out` +
1542
+ (budget === null ? '' : ` of the suite's ${Math.round(budget / 1000)} s per-test budget`));
1543
+ }
1544
+ if (pendingAssets.length > 0 && room > 0) {
1545
+ const shown = pendingAssets.slice(0, room);
1546
+ room -= shown.length;
1547
+ parts.push(`had no recorded response when the test ended; that alone does not establish the cause: ` +
1548
+ shown.map(requestLabel).join('; '));
1416
1549
  }
1417
1550
  if (unanswered.length > 0 && room > 0) {
1418
1551
  const shown = unanswered.slice(0, room);
1419
1552
  room -= shown.length;
1420
- parts.push(`no response recorded — still in flight when the test ended, so not a server failure: ` +
1421
- shown.map(requestLabel).join('; '));
1553
+ parts.push(`no response recorded — still in flight when the test ended, so not a server failure; ` +
1554
+ `that alone does not establish the cause: ${shown.map(requestLabel).join('; ')}`);
1422
1555
  }
1423
1556
  if (answered.length > 0 && room > 0) {
1424
1557
  // An action that sent nothing leaves the page's own load as the only requests there are:
1425
1558
  // name them for what they are, not as the action's answer.
1426
- const label = step !== undefined && !stalledNav && step.requests.length === 0
1559
+ const label = step !== undefined && !stalledNav && stepRequests.length === 0
1427
1560
  ? 'also answered (started before the action)'
1428
1561
  : 'answered';
1429
1562
  parts.push(`${label}: ${answered.slice(0, room).map(requestLine).join('; ')}`);
@@ -1459,22 +1592,30 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1459
1592
  '`MOL_E2E_PREVIEW_PLUGIN=0 npx vite preview --host 127.0.0.1 --port 4173 --strictPort`. '
1460
1593
  : '') +
1461
1594
  'Fix the server before the app.\n\n';
1462
- // A navigation that never finished WITH a subresource that never got a response, and no
1595
+ // A navigation the RUNNER reported failing, with a subresource that never got a response, and no
1463
1596
  // transport error: the page was waiting on the asset when the budget ran out. That is the host
1464
1597
  // or the network serving that file, not the app's state — say it before any app-side reading
1465
1598
  // (xr-molecule-flash-m135 +34:10: three deployed goto timeouts on tests passing locally in
1466
1599
  // 1.2-5.6 s, one 88 KB stylesheet fetch measured at 27 s, and the note said "the cause is in
1467
- // the page").
1600
+ // the page"). Every traced test must meet the bar; one stalled trace beside a selector-wait
1601
+ // failure is not the run's cause (m153).
1468
1602
  const stalledLead = transport !== null || stalledAssets.length === 0
1469
1603
  ? ''
1470
- : `Every failure this run traced is a \`page.goto\` that never finished, and the document's ` +
1471
- `own ${stalledAssets.length === 1 ? 'subresource' : 'subresources'} ${stalledAssets.join(', ')} ` +
1604
+ : `Every failure this run traced is a navigation the runner reported failing ` +
1605
+ `(${stalledWaits.join(', ')}), and the document's own ` +
1606
+ `${stalledAssets.length === 1 ? 'subresource' : 'subresources'} ${stalledAssets.join(', ')} ` +
1472
1607
  `never got a response — the navigation was waiting on ${stalledAssets.length === 1 ? 'it' : 'them'} ` +
1473
1608
  `when the test's budget ran out. That is the host or the network serving the asset, not the ` +
1474
- `app's state or a selector: re-run the same tests once, unchanged, against ${target} before ` +
1475
- `editing anything, and if the stall repeats measure that exact fetch ` +
1476
- `(\`curl -o /dev/null -w '%{time_starttransfer} %{time_total}' <url>\`) before treating it as ` +
1477
- `the app's.\n\n`;
1609
+ `app's state or a selector: ` +
1610
+ (report.productionBuildAdvised
1611
+ ? `the run's own test note already names the next measurement — the same suite against ` +
1612
+ `a production build, because the dev server's page load is not the built app's — take ` +
1613
+ `that one step first, and come back to the app only if the same navigation times out ` +
1614
+ `there too with a stalled asset in the trace.\n\n`
1615
+ : `re-run the same tests once, unchanged, against ${target} before editing anything, ` +
1616
+ `and if the stall repeats measure that exact fetch ` +
1617
+ `(\`curl -o /dev/null -w '%{time_starttransfer} %{time_total}' <url>\`) before ` +
1618
+ `treating it as the app's.\n\n`);
1478
1619
  return (transportLead +
1479
1620
  stalledLead +
1480
1621
  `The platform read the failing tests' own Playwright traces from the run against ${target} ` +
@@ -1493,11 +1634,18 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1493
1634
  'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
1494
1635
  'user action silently — log it.' +
1495
1636
  authAdvice
1496
- : stalledAssets.length > 0
1497
- ? 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
1498
- 'not finish loading because its own subresource never answered. Do not read this as a ' +
1499
- 'page-state bug: re-run these tests against the same URL first, and change the app only ' +
1500
- 'if the same navigation times out again with no stalled asset in the trace.'
1637
+ : anyStalled
1638
+ ? report.productionBuildAdvised
1639
+ ? 'No listed request failed — none got a 4xx/5xx or a network error — but a navigation ' +
1640
+ 'the runner reported failing was still waiting on a document subresource that never ' +
1641
+ 'answered. Do not read this as a page-state bug, and do not re-run the same tests ' +
1642
+ 'unchanged: the run’s own test note names the production build as the next ' +
1643
+ 'measurement — take that one step first, and change the app only if the same ' +
1644
+ 'navigation times out there with no stalled asset in the trace.'
1645
+ : 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
1646
+ 'not finish loading because its own subresource never answered. Do not read this as a ' +
1647
+ 'page-state bug: re-run these tests against the same URL first, and change the app only ' +
1648
+ 'if the same navigation times out again with no stalled asset in the trace.'
1501
1649
  : 'No listed request failed — none got a 4xx/5xx or a network error — so the cause is in the ' +
1502
1650
  "page, not the API. Start with what the test's last action sent (the first line of each " +
1503
1651
  'row) and what the page did next: a request that answered (2xx included) while the page ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.9.12",
3
+ "version": "1.9.13",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",