@molecule/api-ai-tools 1.9.11 → 1.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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:24:21.171Z
6
+ Generated: 2026-10-18T09:17:15.122Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -404,6 +404,28 @@ interface TraceRequest {
404
404
  * fixture), in which case only recording order is available.
405
405
  */
406
406
  startedAt?: number
407
+ /**
408
+ * Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
409
+ *
410
+ * A 401 cannot be read without this: the API's own body said
411
+ * `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
412
+ * when the header's absence proved the CLIENT never attached the token
413
+ * (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
414
+ * refused a token it was sent.
415
+ */
416
+ sentAuthorization?: boolean
417
+ /**
418
+ * The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
419
+ * no header or its first word does not parse as a scheme (a value pasted without one is a
420
+ * credential, and is never copied here).
421
+ */
422
+ authorization?: string
423
+ /**
424
+ * Whether the request carried a `Cookie` header — never the cookies themselves. An app that
425
+ * signs in with a session cookie has no `Authorization` header by design, so this tells a
426
+ * credential-less request from one whose credential is a cookie.
427
+ */
428
+ sentCookie?: boolean
407
429
  }
408
430
  ```
409
431
 
@@ -500,6 +522,52 @@ interface TraceArtifact {
500
522
  }
501
523
  ```
502
524
 
525
+ #### `TraceRunFailure`
526
+
527
+ What the runner's own report says about ONE failing test: where the test is declared, which
528
+ Playwright call its failure names, and the per-test deadline the failure measured against.
529
+
530
+ A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
531
+ xr-molecule-flash-m153 a 15 s `page.waitForSelector` timeout left a trace whose `Frame.goto`
532
+ never finished (the test was cut off mid-navigation) with a `/src/components/*.tsx` module still
533
+ pending when the context closed — and the note read that as an asset hang, then sent the run to
534
+ re-run the same tests unchanged against the same dev server while the same result's
535
+ `testRunNote` was telling it to measure a production build.
536
+
537
+ ```typescript
538
+ interface TraceRunFailure {
539
+ /** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
540
+ spec: string
541
+ /** The line the test is DECLARED on, from that heading (`206`). */
542
+ line: number
543
+ /** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
544
+ operation: string | null
545
+ /** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
546
+ budgetMs: number | null
547
+ }
548
+ ```
549
+
550
+ #### `TraceRunReport`
551
+
552
+ The runner's own report, beside the traces it wrote.
553
+
554
+ ```typescript
555
+ interface TraceRunReport {
556
+ /**
557
+ * One entry per failing test, as the runner printed them. Empty when the caller supplied none:
558
+ * without the runner's reported operation a pending asset is never called the cause.
559
+ */
560
+ failures?: readonly TraceRunFailure[]
561
+ /**
562
+ * True when the same result's `testRunNote` already names a production build as the next
563
+ * measurement (a local dev-server walk that ran out of its per-test budget). The note then
564
+ * states the ONE next step instead of directing an unchanged same-target re-run that
565
+ * contradicts it.
566
+ */
567
+ productionBuildAdvised?: boolean
568
+ }
569
+ ```
570
+
503
571
  #### `DiagnosisTraceRewrite`
504
572
 
505
573
  A command with the trace flag this platform decided to add, and the sentence to report it in.
@@ -1931,7 +1999,7 @@ function stalledDocumentSubresource(request: TraceRequest): boolean
1931
1999
 
1932
2000
  **Returns:** True when the request is a document subresource that never finished.
1933
2001
 
1934
- #### `failureTraceNote(items, target, earlierFailures, apiPrefixes)`
2002
+ #### `failureTraceNote(items, target, earlierFailures, apiPrefixes, report)`
1935
2003
 
1936
2004
  The note a failed suite run's result carries: what each failing test's page actually sent, and
1937
2005
  what came back.
@@ -1963,6 +2031,7 @@ function failureTraceNote(
1963
2031
  target: string,
1964
2032
  earlierFailures?: readonly string[],
1965
2033
  apiPrefixes?: readonly string[],
2034
+ report?: TraceRunReport,
1966
2035
  ): string | null
1967
2036
  ```
1968
2037
 
@@ -1970,6 +2039,7 @@ function failureTraceNote(
1970
2039
  - `target` — Where the run went — the deployed host, as the note names it.
1971
2040
  - `earlierFailures` — The failing `file:line`s earlier walks of this suite left, so a repeat is ranked above a one-off. Empty when the caller knows of no earlier walk.
1972
2041
  - `apiPrefixes` — The API path prefixes the app's client config names (`['/api']`), read from the app by the caller. Empty when the caller could not read one; the traces' own requests may still supply it. Never a hard-coded literal.
2042
+ - `report` — The runner's own report for this run ({@link TraceRunReport}): each failing test's declared location, the operation its failure names and the deadline it names, plus whether the same result's `testRunNote` already points at a production build. A pending asset is called the cause only when the runner says the failing operation for THAT test is the navigation it was loading; without the report nothing is called a navigation stall.
1973
2043
 
1974
2044
  **Returns:** The note, or null when no trace held a recorded response, a real failure or a console error.
1975
2045
 
@@ -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
@@ -67,6 +67,28 @@ export interface TraceRequest {
67
67
  * fixture), in which case only recording order is available.
68
68
  */
69
69
  startedAt?: number;
70
+ /**
71
+ * Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
72
+ *
73
+ * A 401 cannot be read without this: the API's own body said
74
+ * `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
75
+ * when the header's absence proved the CLIENT never attached the token
76
+ * (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
77
+ * refused a token it was sent.
78
+ */
79
+ sentAuthorization?: boolean;
80
+ /**
81
+ * The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
82
+ * no header or its first word does not parse as a scheme (a value pasted without one is a
83
+ * credential, and is never copied here).
84
+ */
85
+ authorization?: string;
86
+ /**
87
+ * Whether the request carried a `Cookie` header — never the cookies themselves. An app that
88
+ * signs in with a session cookie has no `Authorization` header by design, so this tells a
89
+ * credential-less request from one whose credential is a cookie.
90
+ */
91
+ sentCookie?: boolean;
70
92
  }
71
93
  /**
72
94
  * The test's LAST user action before the failing assertion, and the requests it caused.
@@ -257,6 +279,42 @@ export declare function unansweredRequest(request: TraceRequest): boolean;
257
279
  * @returns True when the request is a document subresource that never finished.
258
280
  */
259
281
  export declare function stalledDocumentSubresource(request: TraceRequest): boolean;
282
+ /**
283
+ * What the runner's own report says about ONE failing test: where the test is declared, which
284
+ * Playwright call its failure names, and the per-test deadline the failure measured against.
285
+ *
286
+ * A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
287
+ * xr-molecule-flash-m153 a 15 s `page.waitForSelector` timeout left a trace whose `Frame.goto`
288
+ * never finished (the test was cut off mid-navigation) with a `/src/components/*.tsx` module still
289
+ * pending when the context closed — and the note read that as an asset hang, then sent the run to
290
+ * re-run the same tests unchanged against the same dev server while the same result's
291
+ * `testRunNote` was telling it to measure a production build.
292
+ */
293
+ export interface TraceRunFailure {
294
+ /** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
295
+ spec: string;
296
+ /** The line the test is DECLARED on, from that heading (`206`). */
297
+ line: number;
298
+ /** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
299
+ operation: string | null;
300
+ /** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
301
+ budgetMs: number | null;
302
+ }
303
+ /** The runner's own report, beside the traces it wrote. */
304
+ export interface TraceRunReport {
305
+ /**
306
+ * One entry per failing test, as the runner printed them. Empty when the caller supplied none:
307
+ * without the runner's reported operation a pending asset is never called the cause.
308
+ */
309
+ failures?: readonly TraceRunFailure[];
310
+ /**
311
+ * True when the same result's `testRunNote` already names a production build as the next
312
+ * measurement (a local dev-server walk that ran out of its per-test budget). The note then
313
+ * states the ONE next step instead of directing an unchanged same-target re-run that
314
+ * contradicts it.
315
+ */
316
+ productionBuildAdvised?: boolean;
317
+ }
260
318
  /**
261
319
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
262
320
  * what came back.
@@ -299,12 +357,16 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
299
357
  * transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
300
358
  * rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
301
359
  *
302
- * When a navigation never finished and one of the document's own subresources (a stylesheet, a
303
- * script, an asset under `/assets/`) never got a response either, that request takes a row and the
304
- * note says the page was waiting on it. It is not an API call, but on a `page.goto` timeout it is
305
- * the only request that can explain the timeout — the deployed app was serving the page while one
306
- * fetch stalled for 27 s (xr-molecule-flash-m135 +34:10). The page shell's incidental requests
307
- * (`/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).
308
370
  *
309
371
  * A 404 whose path lacks the API prefix the app's own calls carry is called out with that prefix
310
372
  * ({@link missingApiPrefixRequests}): xr-molecule-flash-m145 posted `/users` to `vite preview`,
@@ -313,6 +375,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
313
375
  * request that never reached the API. The prefix is READ (the caller passes what the app's client
314
376
  * config names; the traces add what their own requests show) and never assumed to be `/api`.
315
377
  *
378
+ * A 401/403 on the app's own origin is answered from the request's own credential record instead
379
+ * ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
380
+ * token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
381
+ * header the API then refused moves the answer to the server. The handler sentence is kept for
382
+ * the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
383
+ * trace that proves the header was never sent cannot be explained by the handler.
384
+ * xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
385
+ * `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
386
+ *
316
387
  * @param items - The traces {@link parseTraceArtifacts} read.
317
388
  * @param target - Where the run went — the deployed host, as the note names it.
318
389
  * @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
@@ -320,10 +391,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
320
391
  * @param apiPrefixes - The API path prefixes the app's client config names (`['/api']`), read from
321
392
  * the app by the caller. Empty when the caller could not read one; the traces' own requests may
322
393
  * still supply it. Never a hard-coded literal.
394
+ * @param report - The runner's own report for this run ({@link TraceRunReport}): each failing
395
+ * test's declared location, the operation its failure names and the deadline it names, plus
396
+ * whether the same result's `testRunNote` already points at a production build. A pending asset
397
+ * is called the cause only when the runner says the failing operation for THAT test is the
398
+ * navigation it was loading; without the report nothing is called a navigation stall.
323
399
  * @returns The note, or null when no trace held a recorded response, a real failure or a console
324
400
  * error.
325
401
  */
326
- export declare function failureTraceNote(items: readonly TraceArtifact[], target: string, earlierFailures?: readonly string[], apiPrefixes?: readonly string[]): string | null;
402
+ export declare function failureTraceNote(items: readonly TraceArtifact[], target: string, earlierFailures?: readonly string[], apiPrefixes?: readonly string[], report?: TraceRunReport): string | null;
327
403
  /**
328
404
  * Whether a Playwright config records traces for the tests that fail.
329
405
  *
@@ -1 +1 @@
1
- {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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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"}
@@ -243,12 +243,34 @@ for (const one of found.slice(0, MAX_TRACES)) {
243
243
  const response = snapshot.response || {}
244
244
  const type = String(snapshot._resourceType || '')
245
245
  if (!origin) origin = type === 'document' ? originOf(String(request.url)) : ''
246
+ // What the request carried for CREDENTIALS — never the secret itself. A 401 on the app's own
247
+ // API cannot be read without this: m148's favorite POST answered 401 with no Authorization
248
+ // header at all, so the client never attached the token, and the note sent the run to the
249
+ // API handler instead (xr-molecule-flash-m148 +29:18). Presence is recorded as a boolean and
250
+ // only the scheme WORD (\`Bearer\`, \`Token\`) is kept; the value is a live credential and never
251
+ // leaves the trace. A first word that does not parse as a scheme (\`Authorization: <raw jwt>\`)
252
+ // is recorded as presence only, so a pasted token can never ride along as a "scheme".
253
+ const headerList = Array.isArray(request.headers) ? request.headers : []
254
+ const authHeader = headerList.find(function (h) {
255
+ return h && typeof h.name === 'string' && /^authorization$/i.test(h.name)
256
+ })
257
+ const authValue =
258
+ authHeader && typeof authHeader.value === 'string' ? String(authHeader.value).trim() : ''
259
+ const authSpace = authValue.search(/\\s/)
260
+ const authWord = authSpace > 0 ? authValue.slice(0, authSpace) : ''
261
+ const authScheme = /^[A-Za-z][A-Za-z0-9._~+/-]{0,23}$/.test(authWord) ? authWord : undefined
262
+ const sentCookie = headerList.some(function (h) {
263
+ return h && typeof h.name === 'string' && /^cookie$/i.test(h.name)
264
+ })
246
265
  requests.push({
247
266
  method: String(request.method).toUpperCase(),
248
267
  path: String(request.url).slice(0, MAX_URL),
249
268
  status: Number(response.status) || 0,
250
269
  failure: typeof response._failureText === 'string' ? response._failureText : undefined,
251
270
  type: type,
271
+ sentAuthorization: authHeader !== undefined,
272
+ authorization: authScheme,
273
+ sentCookie: sentCookie,
252
274
  // The HAR entry carries the monotonic clock the trace's action entries use, so a request
253
275
  // can be attributed to the action that caused it (see \`lastAction\` below).
254
276
  startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
@@ -430,6 +452,12 @@ for (const one of found.slice(0, MAX_TRACES)) {
430
452
  ...(r.sent ? { sent: r.sent } : {}),
431
453
  ...(r.got ? { got: r.got } : {}),
432
454
  type: r.type || undefined,
455
+ // The trace's own clock, so the note can tell a request the navigation was waiting on from
456
+ // one that was already pending before it started (m135/m153).
457
+ ...(typeof r.startedAt === 'number' ? { startedAt: r.startedAt } : {}),
458
+ ...(r.sentAuthorization ? { sentAuthorization: true } : {}),
459
+ ...(r.authorization ? { authorization: r.authorization } : {}),
460
+ ...(r.sentCookie ? { sentCookie: true } : {}),
433
461
  }
434
462
  }
435
463
  // The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
@@ -590,6 +618,12 @@ function parseTraceRequest(value) {
590
618
  ...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
591
619
  ...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
592
620
  ...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
621
+ // The scheme word only: a raw header value is a live credential and is never parsed in.
622
+ ...(r.sentAuthorization === true ? { sentAuthorization: true } : {}),
623
+ ...(typeof r.authorization === 'string' && r.authorization
624
+ ? { authorization: r.authorization }
625
+ : {}),
626
+ ...(r.sentCookie === true ? { sentCookie: true } : {}),
593
627
  };
594
628
  }
595
629
  /**
@@ -934,6 +968,97 @@ function repeatsEarlierFailure(item, earlier) {
934
968
  }
935
969
  return false;
936
970
  }
971
+ /** The Playwright navigation calls whose wait a pending document subresource can explain. */
972
+ const NAVIGATION_OPERATIONS = new Set(['goto', 'waitForURL', 'waitForLoadState', 'reload']);
973
+ /**
974
+ * The method a runner-reported operation names (`page.waitForSelector` → `waitForSelector`), or
975
+ * null when it is not one of the navigation calls a stalled asset can explain.
976
+ *
977
+ * @param operation - The call the runner's error text names, or null.
978
+ * @returns The navigation method, or null.
979
+ */
980
+ function navigationMethod(operation) {
981
+ if (operation === null)
982
+ return null;
983
+ const method = operation.slice(operation.lastIndexOf('.') + 1);
984
+ return NAVIGATION_OPERATIONS.has(method) ? method : null;
985
+ }
986
+ /**
987
+ * The runner's report for the test THIS trace belongs to, or undefined when it belongs to none.
988
+ *
989
+ * The trace names the line of its LAST recorded step; the runner's summary names the test's
990
+ * DECLARATION line, and the step sits inside the body just after it ({@link
991
+ * RECURRENCE_LINE_WINDOW}). A line BEFORE the declaration is a hook — m153's teardown trace sits
992
+ * at `url-navigation.spec.ts:107`, the Pagination suite's `afterEach`, while the failing test is
993
+ * declared at `:206` — and a hook is not the failing test's operation.
994
+ *
995
+ * @param item - One traced failure.
996
+ * @param failures - The runner's report, one entry per failing test.
997
+ * @returns The matching failure, or undefined.
998
+ */
999
+ function runFailureOf(item, failures) {
1000
+ if (item.spec === null)
1001
+ return undefined;
1002
+ for (const failure of failures) {
1003
+ if (!sameLocationFile(item.spec, failure.spec))
1004
+ continue;
1005
+ if (item.line < failure.line)
1006
+ continue;
1007
+ if (item.line - failure.line <= RECURRENCE_LINE_WINDOW)
1008
+ return failure;
1009
+ }
1010
+ return undefined;
1011
+ }
1012
+ /**
1013
+ * Whether a pending request's interval covers a navigation's wait: it started while the
1014
+ * navigation was loading (at or after the wait began) and it has no recorded response, so it was
1015
+ * still pending when the wait ran out. A request that started before the wait belongs to the page
1016
+ * already on screen, not to the navigation that never finished.
1017
+ *
1018
+ * @param request - One request the trace recorded.
1019
+ * @param wait - The navigation action the runner reported.
1020
+ * @returns True when the request's pending interval covers the wait.
1021
+ */
1022
+ function requestCoversWait(request, wait) {
1023
+ if (typeof request.startedAt !== 'number')
1024
+ return false;
1025
+ return request.startedAt >= wait.start - 1;
1026
+ }
1027
+ /**
1028
+ * The stalled-navigation reading for one trace — or null.
1029
+ *
1030
+ * All three facts must hold together: the runner says THIS test's failing operation is a
1031
+ * navigation ({@link NAVIGATION_OPERATIONS}); the trace's record of that call never finished; and
1032
+ * a document subresource still pending at the end started inside that wait
1033
+ * ({@link requestCoversWait}). An asset merely unfinished when the test was cancelled, or one a
1034
+ * teardown/`afterEach` trace holds, meets none of these and is not a cause (m153).
1035
+ *
1036
+ * @param item - One traced failure.
1037
+ * @param report - The runner's report and the run's next step.
1038
+ * @returns The wait and the assets it was waiting on, or null.
1039
+ */
1040
+ function stalledNavigation(item, report) {
1041
+ const failure = runFailureOf(item, report.failures ?? []);
1042
+ if (failure === undefined)
1043
+ return null;
1044
+ const method = navigationMethod(failure.operation);
1045
+ if (method === null)
1046
+ return null;
1047
+ let wait = null;
1048
+ for (const action of item.actions) {
1049
+ if (!action.api.endsWith(`.${method}`))
1050
+ continue;
1051
+ if (wait === null || action.start > wait.start)
1052
+ wait = action;
1053
+ }
1054
+ // The navigation is the failure only while it is still running at the end of the test.
1055
+ if (wait === null || wait.end !== null)
1056
+ return null;
1057
+ const assets = item.requests.filter((request) => stalledDocumentSubresource(request) && requestCoversWait(request, wait));
1058
+ if (assets.length === 0)
1059
+ return null;
1060
+ return { reported: failure.operation ?? wait.api, wait, assets, budgetMs: failure.budgetMs };
1061
+ }
937
1062
  /**
938
1063
  * Whether a trace holds evidence the note can explain: a request that got a RECORDED response, a
939
1064
  * real failure (4xx/5xx or a network error), or a console line. A request with no response is
@@ -943,31 +1068,38 @@ function repeatsEarlierFailure(item, earlier) {
943
1068
  *
944
1069
  * A request the page shell made (m125 +29:33: `/sw.js`, a font) is not evidence about the step
945
1070
  * either, answered or not. The exception is a document subresource that never finished while the
946
- * page's own navigation was still open: the navigation waits on it, so it is the reason the
947
- * `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}).
948
1073
  *
949
1074
  * @param item - One traced failure.
1075
+ * @param report - The runner's report and the run's next step.
950
1076
  * @returns True when the trace holds a request or console line worth a row.
951
1077
  */
952
- function traceRowEvidence(item) {
1078
+ function traceRowEvidence(item, report) {
953
1079
  return (item.console.length > 0 ||
954
1080
  item.requests.some((request) => !isStaticAssetRequest(request) && !unansweredRequest(request)) ||
955
- // The one shape in which a subresource with no response IS the cause: the navigation it was
956
- // part of never finished (see {@link stalledDocumentSubresource}).
957
- (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);
958
1084
  }
959
1085
  /**
960
1086
  * The note's row order: a row with a REAL failure first, then a row whose failure an earlier walk
961
1087
  * also had, then the rest. A recurrence is the only evidence a row carries when its requests were
962
1088
  * all answered (m112's favorite cause: the click's own request answered, the button never
963
1089
  * changed).
1090
+ *
1091
+ * @param item - One traced failure.
1092
+ * @param earlier - The failing `file:line`s earlier walks of this suite left.
1093
+ * @param report - The runner's report and the run's next step.
1094
+ * @returns The row's tier: 0 first, 2 last.
964
1095
  */
965
- function traceRowTier(item, earlier) {
1096
+ function traceRowTier(item, earlier, report) {
966
1097
  if (item.requests.some(failedRequest))
967
1098
  return 0;
968
- // A navigation that never finished because its own subresource never got a response is as
969
- // strong a cause as a failed request — it is the one fact the timeout was waiting on (m135).
970
- 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)
971
1103
  return 0;
972
1104
  if (repeatsEarlierFailure(item, earlier))
973
1105
  return 1;
@@ -1031,6 +1163,94 @@ function sameOriginPath(request) {
1031
1163
  return null;
1032
1164
  return raw.replace(/\/+$/, '');
1033
1165
  }
1166
+ /**
1167
+ * The sentence a 401/403 on the app's own API gets: which half of the wire refused the request.
1168
+ *
1169
+ * A fixed "read the handler this route reaches" line is the wrong half for a credential refusal.
1170
+ * The request's own `Authorization` header — recorded by the trace reader as its scheme WORD, never
1171
+ * its value — decides: absent means the CLIENT never attached the token (a session store that the
1172
+ * HTTP client does not read, a bare `fetch`, an auth bond the app replaced), while present means
1173
+ * the SERVER refused a token it was sent (scheme or issuer). A 403 is a permission answer, not a
1174
+ * credential one, and says so.
1175
+ *
1176
+ * xr-molecule-flash-m148 (+29:18): `POST /api/articles/…/favorite → 401 {"errors":{"token":["is
1177
+ * missing"]}}` was answered with the handler sentence for six local walks, while the trace proved
1178
+ * the request carried no `Authorization` header at all and the app's hand-rolled session store
1179
+ * never handed its token to the shared client.
1180
+ *
1181
+ * The trace cannot name the file that made the call: a real HAR entry carries the request, response,
1182
+ * `_resourceType`, `_monotonicTime` and a frame ref, but no initiator stack. So the sentence names
1183
+ * the shared client and the auth bond instead of guessing a file.
1184
+ *
1185
+ * @param request - The refused request, a 401 or 403 on the app's own origin.
1186
+ * @returns The sentence the note appends.
1187
+ */
1188
+ function authRefusalSentence(request) {
1189
+ const label = requestLabel(request);
1190
+ // The header's scheme, when it parsed; presence alone is still the server's half of the wire.
1191
+ const carried = request.authorization !== undefined
1192
+ ? `with \`Authorization: ${request.authorization} …\``
1193
+ : 'with an `Authorization` header';
1194
+ if (request.status === 403) {
1195
+ return request.sentAuthorization === true
1196
+ ? `The browser sent \`${label}\` ${carried} and the API answered 403 — the token was ` +
1197
+ 'accepted but this user may not do it: read the route’s role/permission check in your ' +
1198
+ 'own source, and the account the session belongs to.'
1199
+ : `The browser sent \`${label}\` and the API answered 403 — the request carried no ` +
1200
+ '`Authorization` header, so either the client did not attach the token (check that the ' +
1201
+ 'request goes through the app’s HTTP client that the auth bond configures, and that the ' +
1202
+ 'token is stored where the client reads it) or the route refuses this user: read the ' +
1203
+ 'route’s role/permission check in your own source.';
1204
+ }
1205
+ if (request.sentAuthorization === true) {
1206
+ return (`The browser sent \`${label}\` ${carried} and the API refused it: the token was sent and ` +
1207
+ 'rejected — the scheme (the client’s `authScheme`, e.g. `Token` vs `Bearer`) or the ' +
1208
+ 'token’s issuer differs from what the server verifies; read the verify middleware this ' +
1209
+ 'route reaches.');
1210
+ }
1211
+ return (`The browser sent \`${label}\` with NO \`Authorization\` header — the client did not attach ` +
1212
+ 'the token: check that the request goes through the app’s HTTP client that the auth bond ' +
1213
+ 'configures (not a bare `fetch`), and that the token is stored where the client reads it. ' +
1214
+ 'The shared client carries it through `setAuthToken` — the auth bond ' +
1215
+ '(`createDefaultAuthClientWithHttpSync` in `@molecule/app-bonds-default-react`) mirrors the ' +
1216
+ 'auth client’s token there, and a session store of your own has to call it too. ' +
1217
+ (request.sentCookie
1218
+ ? 'The request did carry a `Cookie` header, so if this app signs in with a session cookie ' +
1219
+ 'that is the credential the server did not accept. '
1220
+ : '') +
1221
+ 'The API handler is not the problem.');
1222
+ }
1223
+ /**
1224
+ * The note's credential-refusal advice, one sentence per distinct shape — at most two.
1225
+ *
1226
+ * The shapes are `(status, Authorization scheme, cookie present)`: m148's ten walks repeated one
1227
+ * shape (`401`, no header) on every failing test, and repeating its sentence per row would bury
1228
+ * the fact rather than state it.
1229
+ *
1230
+ * @param items - The traces the note is about.
1231
+ * @returns The sentences to append (each led by a space), or '' when no same-origin 401/403 was
1232
+ * traced.
1233
+ */
1234
+ function authRefusalAdvice(items) {
1235
+ const sentences = [];
1236
+ const seen = new Set();
1237
+ for (const item of items) {
1238
+ for (const request of item.requests) {
1239
+ if (request.status !== 401 && request.status !== 403)
1240
+ continue;
1241
+ if (sameOriginPath(request) === null)
1242
+ continue;
1243
+ const key = `${request.status} ${request.sentAuthorization ? (request.authorization ?? 'header') : 'none'} ${request.sentCookie ? 'cookie' : ''}`;
1244
+ if (seen.has(key))
1245
+ continue;
1246
+ seen.add(key);
1247
+ sentences.push(authRefusalSentence(request));
1248
+ if (sentences.length === 2)
1249
+ return ` ${sentences.join(' ')}`;
1250
+ }
1251
+ }
1252
+ return sentences.length === 0 ? '' : ` ${sentences.join(' ')}`;
1253
+ }
1034
1254
  /**
1035
1255
  * The API path prefixes the traced requests themselves show.
1036
1256
  *
@@ -1148,12 +1368,16 @@ function missingApiPrefixSentence(request) {
1148
1368
  * transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
1149
1369
  * rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
1150
1370
  *
1151
- * When a navigation never finished and one of the document's own subresources (a stylesheet, a
1152
- * script, an asset under `/assets/`) never got a response either, that request takes a row and the
1153
- * note says the page was waiting on it. It is not an API call, but on a `page.goto` timeout it is
1154
- * the only request that can explain the timeout — the deployed app was serving the page while one
1155
- * fetch stalled for 27 s (xr-molecule-flash-m135 +34:10). The page shell's incidental requests
1156
- * (`/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).
1157
1381
  *
1158
1382
  * A 404 whose path lacks the API prefix the app's own calls carry is called out with that prefix
1159
1383
  * ({@link missingApiPrefixRequests}): xr-molecule-flash-m145 posted `/users` to `vite preview`,
@@ -1162,6 +1386,15 @@ function missingApiPrefixSentence(request) {
1162
1386
  * request that never reached the API. The prefix is READ (the caller passes what the app's client
1163
1387
  * config names; the traces add what their own requests show) and never assumed to be `/api`.
1164
1388
  *
1389
+ * A 401/403 on the app's own origin is answered from the request's own credential record instead
1390
+ * ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
1391
+ * token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
1392
+ * header the API then refused moves the answer to the server. The handler sentence is kept for
1393
+ * the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
1394
+ * trace that proves the header was never sent cannot be explained by the handler.
1395
+ * xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
1396
+ * `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
1397
+ *
1165
1398
  * @param items - The traces {@link parseTraceArtifacts} read.
1166
1399
  * @param target - Where the run went — the deployed host, as the note names it.
1167
1400
  * @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
@@ -1169,17 +1402,22 @@ function missingApiPrefixSentence(request) {
1169
1402
  * @param apiPrefixes - The API path prefixes the app's client config names (`['/api']`), read from
1170
1403
  * the app by the caller. Empty when the caller could not read one; the traces' own requests may
1171
1404
  * still supply it. Never a hard-coded literal.
1405
+ * @param report - The runner's own report for this run ({@link TraceRunReport}): each failing
1406
+ * test's declared location, the operation its failure names and the deadline it names, plus
1407
+ * whether the same result's `testRunNote` already points at a production build. A pending asset
1408
+ * is called the cause only when the runner says the failing operation for THAT test is the
1409
+ * navigation it was loading; without the report nothing is called a navigation stall.
1172
1410
  * @returns The note, or null when no trace held a recorded response, a real failure or a console
1173
1411
  * error.
1174
1412
  */
1175
- export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = []) {
1176
- const candidates = items.filter(traceRowEvidence);
1413
+ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = [], report = {}) {
1414
+ const candidates = items.filter((item) => traceRowEvidence(item, report));
1177
1415
  if (candidates.length === 0)
1178
1416
  return null;
1179
1417
  // Ranked, then deduplicated BY LOCATION — in that order, so the row kept for a location is the
1180
1418
  // one carrying the strongest evidence (m112's duplicate `afterEach` rows).
1181
1419
  const ranked = candidates
1182
- .map((item, index) => ({ item, index, tier: traceRowTier(item, earlierFailures) }))
1420
+ .map((item, index) => ({ item, index, tier: traceRowTier(item, earlierFailures, report) }))
1183
1421
  .sort((left, right) => left.tier - right.tier || left.index - right.index);
1184
1422
  const traced = [];
1185
1423
  const seen = new Set();
@@ -1212,14 +1450,25 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1212
1450
  const prefixAdvice = missingPrefix.length > 0
1213
1451
  ? ` ${missingPrefix.slice(0, 2).map(missingApiPrefixSentence).join(' ')}`
1214
1452
  : '';
1215
- // The document subresources that never finished anywhere in this read — the assets a timed-out
1216
- // navigation was waiting on, when there is no failed request to explain the timeout.
1217
- const stalledAssets = [
1218
- ...new Set(traced
1219
- .filter(navigationNeverFinished)
1220
- .flatMap((item) => item.requests.filter(stalledDocumentSubresource))
1221
- .map(requestLabel)),
1453
+ // A credential refusal (401/403 on the app's own API) is not the handler's answer: the trace's
1454
+ // own `Authorization` record says whether the CLIENT attached the token at all (m148 +29:18).
1455
+ const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null && (request.status === 401 || request.status === 403));
1456
+ const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
1457
+ const authAdvice = authRefusalAdvice(traced);
1458
+ // The stalled-navigation reading, per traced test: non-null only when the RUNNER says this
1459
+ // test's failing operation is a navigation and a document subresource still pending at the end
1460
+ // started inside that wait ({@link stalledNavigation}).
1461
+ const stalls = traced.map((item) => stalledNavigation(item, report));
1462
+ // "Every failure this run traced…" is only honest when every traced test meets that bar — one
1463
+ // stalled trace beside a selector-wait failure is not the cause of the run (m153).
1464
+ const everyTracedStalls = stalls.every((stall) => stall !== null);
1465
+ const stalledAssets = everyTracedStalls
1466
+ ? [...new Set(stalls.flatMap((stall) => stall?.assets.map(requestLabel) ?? []))]
1467
+ : [];
1468
+ const stalledWaits = [
1469
+ ...new Set(stalls.flatMap((stall) => (stall === null ? [] : [stall.reported]))),
1222
1470
  ];
1471
+ const anyStalled = stalls.some((stall) => stall !== null);
1223
1472
  const rows = [];
1224
1473
  for (const [index, item] of traced.entries()) {
1225
1474
  // The test's LAST action before the failing assertion leads the row (m148): the requests it
@@ -1227,10 +1476,24 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1227
1476
  // page's own last call (a redirect target's `GET /api/tags`) and the shell's files do not.
1228
1477
  const lead = item.lead;
1229
1478
  const step = item.lastAction;
1230
- // A navigation that never finished has one explanation — the stalled asset it was waiting on
1231
- // (m135) — and that leads the row instead of the action's own document request.
1232
- const stalledNav = navigationNeverFinished(item) && item.requests.some(stalledDocumentSubresource);
1233
- const stepRequests = step !== undefined && !stalledNav ? step.requests : [];
1479
+ // A navigation the runner reported, still waiting on a stalled asset (m135), leads the row
1480
+ // instead of the action's own document request.
1481
+ const stall = stalls[index] ?? null;
1482
+ const stalledNav = stall !== null;
1483
+ // The document subresources that never finished inside the navigation the runner reported:
1484
+ // the page was waiting on them when the test's budget ran out (m135).
1485
+ const stalled = stall?.assets ?? [];
1486
+ // The same kind of pending asset on a trace the runner does NOT report as a navigation
1487
+ // failure (a selector wait that ran out the test budget, an `afterEach`/teardown trace): it is
1488
+ // not a cause, and it is still worth naming so the executor does not have to guess (m153).
1489
+ const stalledKeys = new Set(stalled.map(requestKey));
1490
+ const pendingAssets = item.requests.filter((request) => stalledDocumentSubresource(request) && !stalledKeys.has(requestKey(request)));
1491
+ const pendingKeys = new Set(pendingAssets.map(requestKey));
1492
+ // A pending asset says nothing about what the action got back, so it never rides the action
1493
+ // lead — the honest bucket below names it (m153).
1494
+ const stepRequests = step !== undefined && !stalledNav
1495
+ ? step.requests.filter((request) => !pendingKeys.has(requestKey(request)))
1496
+ : [];
1234
1497
  const stepKeys = new Set(stepRequests.map(requestKey));
1235
1498
  const isLead = (request) => lead !== undefined && requestKey(request) === requestKey(lead);
1236
1499
  // The page shell's own requests take no row, in any bucket: an aborted `/sw.js` would
@@ -1239,20 +1502,16 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1239
1502
  const failed = requests.filter(failedRequest);
1240
1503
  const unanswered = requests.filter(unansweredRequest);
1241
1504
  const answered = requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
1242
- // A document subresource that never finished, on a trace whose navigation never finished:
1243
- // the page was waiting on it when the test's budget ran out (m135).
1244
- const stalled = navigationNeverFinished(item)
1245
- ? item.requests.filter(stalledDocumentSubresource)
1246
- : [];
1505
+ const budget = stall?.budgetMs ?? null;
1247
1506
  let room = MAX_TRACE_NOTE_REQUESTS;
1248
1507
  const parts = [];
1249
1508
  if (step !== undefined && !stalledNav) {
1250
1509
  const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
1251
- if (step.requests.length === 0) {
1510
+ if (stepRequests.length === 0) {
1252
1511
  parts.push(`${head}, and it sent no request`);
1253
1512
  }
1254
1513
  else {
1255
- const shown = step.requests.slice(0, room);
1514
+ const shown = stepRequests.slice(0, room);
1256
1515
  room -= shown.length;
1257
1516
  // The response body is the point (m148: the register `POST /api/users` answered 201 and
1258
1517
  // the page rendered signed-out anyway) — printed whatever the status.
@@ -1279,18 +1538,25 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1279
1538
  const shown = stalled.slice(0, room);
1280
1539
  room -= shown.length;
1281
1540
  parts.push(`never finished: ${shown.map(requestLabel).join('; ')} — a document subresource with no ` +
1282
- '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('; '));
1283
1549
  }
1284
1550
  if (unanswered.length > 0 && room > 0) {
1285
1551
  const shown = unanswered.slice(0, room);
1286
1552
  room -= shown.length;
1287
- parts.push(`no response recorded — still in flight when the test ended, so not a server failure: ` +
1288
- shown.map(requestLabel).join('; '));
1553
+ parts.push(`no response recorded — still in flight when the test ended, so not a server failure; ` +
1554
+ `that alone does not establish the cause: ${shown.map(requestLabel).join('; ')}`);
1289
1555
  }
1290
1556
  if (answered.length > 0 && room > 0) {
1291
1557
  // An action that sent nothing leaves the page's own load as the only requests there are:
1292
1558
  // name them for what they are, not as the action's answer.
1293
- const label = step !== undefined && !stalledNav && step.requests.length === 0
1559
+ const label = step !== undefined && !stalledNav && stepRequests.length === 0
1294
1560
  ? 'also answered (started before the action)'
1295
1561
  : 'answered';
1296
1562
  parts.push(`${label}: ${answered.slice(0, room).map(requestLine).join('; ')}`);
@@ -1326,22 +1592,30 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1326
1592
  '`MOL_E2E_PREVIEW_PLUGIN=0 npx vite preview --host 127.0.0.1 --port 4173 --strictPort`. '
1327
1593
  : '') +
1328
1594
  'Fix the server before the app.\n\n';
1329
- // A navigation 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
1330
1596
  // transport error: the page was waiting on the asset when the budget ran out. That is the host
1331
1597
  // or the network serving that file, not the app's state — say it before any app-side reading
1332
1598
  // (xr-molecule-flash-m135 +34:10: three deployed goto timeouts on tests passing locally in
1333
1599
  // 1.2-5.6 s, one 88 KB stylesheet fetch measured at 27 s, and the note said "the cause is in
1334
- // the page").
1600
+ // the page"). Every traced test must meet the bar; one stalled trace beside a selector-wait
1601
+ // failure is not the run's cause (m153).
1335
1602
  const stalledLead = transport !== null || stalledAssets.length === 0
1336
1603
  ? ''
1337
- : `Every failure this run traced is a \`page.goto\` that never finished, and the document's ` +
1338
- `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(', ')} ` +
1339
1607
  `never got a response — the navigation was waiting on ${stalledAssets.length === 1 ? 'it' : 'them'} ` +
1340
1608
  `when the test's budget ran out. That is the host or the network serving the asset, not the ` +
1341
- `app's state or a selector: re-run the same tests once, unchanged, against ${target} before ` +
1342
- `editing anything, and if the stall repeats measure that exact fetch ` +
1343
- `(\`curl -o /dev/null -w '%{time_starttransfer} %{time_total}' <url>\`) before treating it as ` +
1344
- `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`);
1345
1619
  return (transportLead +
1346
1620
  stalledLead +
1347
1621
  `The platform read the failing tests' own Playwright traces from the run against ${target} ` +
@@ -1352,14 +1626,26 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1352
1626
  (anyFailed
1353
1627
  ? everyFailureLacksPrefix
1354
1628
  ? ''
1355
- : 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
1356
- 'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
1357
- 'user action silently — log it.'
1358
- : stalledAssets.length > 0
1359
- ? 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
1360
- 'not finish loading because its own subresource never answered. Do not read this as a ' +
1361
- 'page-state bug: re-run these tests against the same URL first, and change the app only ' +
1362
- 'if the same navigation times out again with no stalled asset in the trace.'
1629
+ : everyFailureAuth
1630
+ ? // Every failure was a credential refusal: the handler is not the problem, and the
1631
+ // auth advice already names what is (m148 was sent to the handler six walks running).
1632
+ authAdvice
1633
+ : 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
1634
+ 'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
1635
+ 'user action silently — log it.' +
1636
+ authAdvice
1637
+ : anyStalled
1638
+ ? report.productionBuildAdvised
1639
+ ? 'No listed request failed — none got a 4xx/5xx or a network error — but a navigation ' +
1640
+ 'the runner reported failing was still waiting on a document subresource that never ' +
1641
+ 'answered. Do not read this as a page-state bug, and do not re-run the same tests ' +
1642
+ 'unchanged: the run’s own test note names the production build as the next ' +
1643
+ 'measurement — take that one step first, and change the app only if the same ' +
1644
+ 'navigation times out there with no stalled asset in the trace.'
1645
+ : 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
1646
+ 'not finish loading because its own subresource never answered. Do not read this as a ' +
1647
+ 'page-state bug: re-run these tests against the same URL first, and change the app only ' +
1648
+ 'if the same navigation times out again with no stalled asset in the trace.'
1363
1649
  : 'No listed request failed — none got a 4xx/5xx or a network error — so the cause is in the ' +
1364
1650
  "page, not the API. Start with what the test's last action sent (the first line of each " +
1365
1651
  'row) and what the page did next: a request that answered (2xx included) while the page ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.9.11",
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",