@molecule/api-ai-tools 1.9.2 → 1.9.3

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/dist/tools.js CHANGED
@@ -359,6 +359,7 @@ async function backgroundTaskState(backend, command, seconds, ceilingMs) {
359
359
  slice,
360
360
  };
361
361
  }
362
+ import { suiteCommandTarget } from './suite-overlap.js';
362
363
  import { diagnosisTraceCommand } from './trace-artifacts.js';
363
364
  import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, earlyReturnCause, emptyCallError, failureCausesNote, findPathArgs, findPatternArgs, fusedLine, htmlOutline, isCommandTimeout, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isSuiteRunCommand, isValidGlob, linesWithinSerialized, literalDirPrefix, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, searchTimeoutError, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPipelinePlacement, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
364
365
  /**
@@ -578,6 +579,35 @@ function batchFailureError(path, total, failures) {
578
579
  ? `\n\n(${omitted} more replacement${omitted === 1 ? '' : 's'} also failed and are not shown.)`
579
580
  : ''));
580
581
  }
582
+ /**
583
+ * Delays between retries of the symlink guard while a sandbox's command channel is
584
+ * still attaching: four attempts over roughly 2.6 s (plus the commands' own time),
585
+ * which covers the span a freshly claimed E2B sandbox answers `SandboxNotFoundError`
586
+ * before its exec channel is usable (xr-molecule-flash-m120 +00:29). A plain slow
587
+ * answer keeps the single 300 ms retry instead — a sandbox that is gone does not
588
+ * come back inside five seconds.
589
+ */
590
+ const SANDBOX_STARTING_BACKOFF_MS = [300, 800, 1_500];
591
+ /**
592
+ * Whether an error from the sandbox's command channel says the sandbox is still
593
+ * STARTING rather than gone for good.
594
+ *
595
+ * A sandbox created seconds earlier answers `SandboxNotFoundError` ("Sandbox is
596
+ * probably not running anymore") until its channel attaches, and that message names
597
+ * the worse of the two causes — so a file tool refused it as if the sandbox were
598
+ * dead. These shapes are retried; anything else keeps the one quick retry.
599
+ *
600
+ * @param error - The error thrown by `backend.run`.
601
+ * @returns True when the sandbox looks like it is still coming up.
602
+ */
603
+ function sandboxStillStarting(error) {
604
+ const err = error;
605
+ const name = typeof err?.name === 'string' ? err.name : '';
606
+ const message = typeof err?.message === 'string' ? err.message : '';
607
+ if (/^SandboxNotFound(?:Error)?$/i.test(name))
608
+ return true;
609
+ return /not (?:found|running)|notfound|not attached|still starting/i.test(`${name} ${message}`);
610
+ }
581
611
  /**
582
612
  * Build a complete set of AI agent tools bound to an execution backend.
583
613
  *
@@ -711,10 +741,18 @@ export function buildTools(backend, config) {
711
741
  async function checkSymlink(path) {
712
742
  if (!symlinkGuards)
713
743
  return null;
714
- // One retry: the first commands on a just-attached sandbox can fail at once (X1
715
- // flash-m44: three paths refused in 332 ms, the same check passing a beat later).
744
+ // The retry budget is failure-aware. A plain slow answer gets ONE quick retry
745
+ // (X1 flash-m44: three paths refused in 332 ms, the same check passing a beat
746
+ // later). A channel that is still ATTACHING gets longer: a sandbox created
747
+ // seconds earlier answers `SandboxNotFoundError` for well over 600 ms, so the
748
+ // fixed 2 × 300 ms budget ran out and the run's FIRST file tool was refused with
749
+ // "probably not running anymore" 300 ms before the same tool answered
750
+ // (xr-molecule-flash-m120 +00:29). Those errors are about a sandbox that is
751
+ // still starting, not one that is gone, so they are retried up to ~5 s.
716
752
  let lastError;
717
- for (let attempt = 0; attempt < 2; attempt++) {
753
+ let attempts = 0;
754
+ for (;;) {
755
+ attempts++;
718
756
  const started = Date.now();
719
757
  try {
720
758
  const result = await backend.run(`readlink -f ${shellQuote(path)} 2>/dev/null || echo ${shellQuote(path)}`, { timeout: 5000 });
@@ -728,20 +766,29 @@ export function buildTools(backend, config) {
728
766
  lastError = Object.assign(error instanceof Error ? error : new Error(String(error)), {
729
767
  elapsedMs: Date.now() - started,
730
768
  });
731
- if (attempt === 0)
732
- await new Promise((resolve) => setTimeout(resolve, 300));
769
+ const backoff = sandboxStillStarting(lastError) ? SANDBOX_STARTING_BACKOFF_MS : [];
770
+ const wait = backoff[attempts - 1] ?? (attempts === 1 ? 300 : undefined);
771
+ if (wait === undefined)
772
+ break;
773
+ await new Promise((resolve) => setTimeout(resolve, wait));
733
774
  }
734
775
  }
735
776
  // Deny conservatively, and say what failed: "access denied" read as a forbidden path,
736
777
  // and a fixed "did not answer within 5 s" hid an error that took 332 ms.
737
778
  const err = lastError;
738
779
  const timedOut = err?.name === 'TimeoutError' || (err?.elapsedMs ?? 0) >= 4900;
739
- return timedOut
740
- ? 'Unable to verify path safety: the sandbox did not answer the check within 5 s (it may be ' +
780
+ if (timedOut) {
781
+ return ('Unable to verify path safety: the sandbox did not answer the check within 5 s (it may be ' +
741
782
  'restarting or out of memory), so the path was refused to stay safe. Retry in a moment; if ' +
742
- 'every file tool fails this way, the sandbox needs a restart.'
743
- : `Unable to verify path safety: the sandbox's command channel failed (${err?.name ?? 'Error'}: ${String(err?.message ?? err).slice(0, 160)}), ` +
744
- 'so the path was refused to stay safe. Retry the call; if every file tool fails this way, the sandbox needs a restart.';
783
+ 'every file tool fails this way, the sandbox needs a restart.');
784
+ }
785
+ if (sandboxStillStarting(err)) {
786
+ return ('Unable to verify path safety: the sandbox is still starting (its command channel is not ' +
787
+ 'attached yet), so the path was refused to stay safe. Retry in a few seconds — a freshly ' +
788
+ 'created sandbox can take a moment to attach.');
789
+ }
790
+ return (`Unable to verify path safety: the sandbox's command channel failed (${err?.name ?? 'Error'}: ${String(err?.message ?? err).slice(0, 160)}), ` +
791
+ 'so the path was refused to stay safe. Retry the call; if every file tool fails this way, the sandbox needs a restart.');
745
792
  }
746
793
  /**
747
794
  * Strip control characters and optionally redact secrets from tool output.
@@ -2506,6 +2553,13 @@ export function buildTools(backend, config) {
2506
2553
  // note it printed called "a lead, not a cause" — and read the early return as "stop waiting,
2507
2554
  // edit now", voiding a run that was still walking both times (the second wait, 224 s of a
2508
2555
  // 280 s budget, ended with nothing at all).
2556
+ // How this run was started, so the note can hand over the re-run to send once it ends
2557
+ // instead of one for now — the platform refuses a second run against the same app and
2558
+ // database while this one walks (xr-molecule-flash-m89: the +19:59 note named a re-run
2559
+ // and its overlap hold refused that exact run 23 s later) — and so a run narrowed to a
2560
+ // file or two is judged against what it selected (xr-molecule-flash-m119 +42:08). Read
2561
+ // BEFORE the loop: the running answers below are built after it closes and still need it.
2562
+ const started = runByTask.get(taskId);
2509
2563
  let midRunNote = null;
2510
2564
  let lastCauseLookAt = startedAt - repeatingCauseLookMs();
2511
2565
  let exitCode = await readExit();
@@ -2537,12 +2591,7 @@ export function buildTools(backend, config) {
2537
2591
  continue;
2538
2592
  // The ONE predicate behind this decision and `repeatingFailureNote`'s wording, so the two
2539
2593
  // cannot come to different verdicts.
2540
- const stopCause = earlyReturnCause(found, universalCause(causeLog, found));
2541
- // How this run was started, so the note can hand over the re-run to send once it ends
2542
- // instead of one for now — the platform refuses a second run against the same app and
2543
- // database while this one walks (xr-molecule-flash-m89: the +19:59 note named a re-run
2544
- // and its overlap hold refused that exact run 23 s later).
2545
- const started = runByTask.get(taskId);
2594
+ const stopCause = earlyReturnCause(found, universalCause(causeLog, found, started?.command));
2546
2595
  if (stopCause) {
2547
2596
  // …and only now is the pid walk paid for.
2548
2597
  repeatingCauseNoted.add(taskId);
@@ -2567,12 +2616,18 @@ export function buildTools(backend, config) {
2567
2616
  // duration into a proven whole-run cause, and that one is worth returning early for.
2568
2617
  if (!midRunLeadNoted.has(taskId)) {
2569
2618
  midRunLeadNoted.add(taskId);
2619
+ // A run against the sandbox's own app is changed by an edit while it walks — the two
2620
+ // instructions the note and the wait's own sentence give must agree (xr-molecule-flash-
2621
+ // m122 +19:00: one result said editing app/src "changes the code the run is testing" and
2622
+ // "Start fixing it now"). The target is read off the command the task was started with.
2623
+ const editsInvalidateRun = started === undefined || suiteCommandTarget(started.command) === 'local';
2570
2624
  midRunNote = repeatingFailureNote(found, {
2571
2625
  taskId,
2572
2626
  log,
2573
2627
  stop: '',
2574
2628
  universal: null,
2575
2629
  ...(started ? { run: started } : {}),
2630
+ ...(editsInvalidateRun ? { editsInvalidateRun: true } : {}),
2576
2631
  });
2577
2632
  }
2578
2633
  }
@@ -2663,7 +2718,16 @@ export function buildTools(backend, config) {
2663
2718
  `each failure's own report into the log as that test fails, right under its ✘ line, ` +
2664
2719
  `not only in the summary at the end. `
2665
2720
  : `Nothing has been printed yet — \`grep -n '✘' ${log} | head -40\` lists failures as they appear. `) +
2666
- `Meanwhile do work that leaves the code under test alone (read the failures so far, plan the fixes): editing app/src or api/src now changes the code the run is testing, and its result no longer counts.${midRun}`,
2721
+ // What an edit does to this run depends on what the run MEASURES: the sandbox's own
2722
+ // app is re-served from app/src as it is edited, so the run's result stops counting; a
2723
+ // run against the deployed build is untouched by a local edit, and the executor can
2724
+ // spend the rest of the run fixing (xr-molecule-flash-m122 +19:00: a deployed run was
2725
+ // told editing changes what it tests, then handed "Start fixing it now" in the same
2726
+ // result — it waited out 1 min 11 s of free fixing time).
2727
+ (started !== undefined && suiteCommandTarget(started.command) !== 'local'
2728
+ ? `Meanwhile fix what it has already named: this run tests the deployed build, so editing app/src or api/src now does not change what it tests — fix the causes it has already named while it walks, run nothing against the app until it ends, then deploy once.`
2729
+ : `Meanwhile do work that leaves the code under test alone (read the failures so far, plan the fixes): editing app/src or api/src now changes the code the run is testing, and its result no longer counts.`) +
2730
+ `${midRun}`,
2667
2731
  };
2668
2732
  }
2669
2733
  let output;
@@ -54,6 +54,12 @@ export interface TraceRequest {
54
54
  sent?: string;
55
55
  /** What came back for that 4xx, bounded the same way — the API's own error body. */
56
56
  got?: string;
57
+ /**
58
+ * The browser's own resource type (`document`, `fetch`, `xhr`, `font`, `image`, `stylesheet`,
59
+ * `script`, …). `document` when the request was the page load itself; when it ended in a
60
+ * `net::ERR_*`, nothing rendered and no selector or app request was measured.
61
+ */
62
+ type?: string;
57
63
  }
58
64
  /** One action the traced page performed, with the trace's own timing for it. */
59
65
  export interface TraceAction {
@@ -137,6 +143,36 @@ export declare const traceArtifactsScript: () => string;
137
143
  * @returns One entry per trace it printed.
138
144
  */
139
145
  export declare function parseTraceArtifacts(stdout: string): TraceArtifact[];
146
+ /**
147
+ * Whether a request is one of the page shell's, not the test's (`STATIC_ASSET_PATH`).
148
+ *
149
+ * The document itself is never incidental — a page that never loaded IS the failure — and a
150
+ * request whose type is the app's own API (`fetch`/`xhr`) is judged by its path alone.
151
+ *
152
+ * @param request - One request a trace recorded.
153
+ * @returns True when the request is incidental to the step under test.
154
+ */
155
+ export declare function isStaticAssetRequest(request: TraceRequest): boolean;
156
+ /**
157
+ * Whether a trace recorded a request the server actually failed: a 4xx/5xx, or a network error.
158
+ *
159
+ * A 404 on a static asset is excepted: `/sw.js` and `/fonts/*` 404 on an app that never shipped
160
+ * them, and naming one as a failure sent m125 into writing the caching worker it had asked about.
161
+ * A 5xx on an asset, or a network error, still counts — the server did fail.
162
+ *
163
+ * @param request - One request a trace recorded.
164
+ * @returns True when the request failed in a way the note should name.
165
+ */
166
+ export declare function failedRequest(request: TraceRequest): boolean;
167
+ /**
168
+ * Whether a trace recorded NO response and no reason for it — a request that was aborted or still
169
+ * in flight when the test ended. It is not evidence that the server failed (m112 +30:45), and one
170
+ * the page shell made is not evidence about the step at all (m125 +29:33).
171
+ *
172
+ * @param request - One request a trace recorded.
173
+ * @returns True when the request is still in flight and worth naming.
174
+ */
175
+ export declare function unansweredRequest(request: TraceRequest): boolean;
140
176
  /**
141
177
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
142
178
  * what came back.
@@ -164,6 +200,10 @@ export declare function parseTraceArtifacts(stdout: string): TraceArtifact[];
164
200
  * traces of one `afterEach` line whose every request was `-1`, and left the favorite cause — the
165
201
  * one failure that repeated in every walk — in its "… and N more".
166
202
  *
203
+ * A run whose every traced page never finished its navigation leads with the browser's own
204
+ * transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
205
+ * rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
206
+ *
167
207
  * @param items - The traces {@link parseTraceArtifacts} read.
168
208
  * @param target - Where the run went — the deployed host, as the note names it.
169
209
  * @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
@@ -1 +1 @@
1
- {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;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,iGAAiG;AACjG,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,0DAA0D;IAC1D,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB;;;;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,4DAA4D;AAC5D,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;AAKvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAO,MAiQvC,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAqCnE;AAwJD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,SAAS,MAAM,EAAO,GACtC,MAAM,GAAG,IAAI,CAmFf;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,8BAA8B,CAAA;AA2G/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,CAyBjC"}
1
+ {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
@@ -244,9 +244,24 @@ for (const one of found.slice(0, MAX_TRACES)) {
244
244
  if (sent) r.sent = sent
245
245
  if (got) r.got = got
246
246
  }
247
+ // The page shell's own requests (a service worker probe, a manifest, a font) are not the
248
+ // failing step's dependencies: a 404 on one, or no response at all, is not a cause and takes
249
+ // no row (xr-molecule-flash-m125 +29:33 — "GET /sw.js; GET /fonts/ionicons.woff" sent the
250
+ // model to write a caching service worker into the app).
251
+ const SHELL_TYPE = { font: 1, image: 1, stylesheet: 1, script: 1, manifest: 1 }
252
+ function shellPath(path) {
253
+ return /^\\/(?:sw|service-worker)\\.js(?:[?#]|$)|\\/manifest\\.webmanifest(?:[?#]|$)|\\/favicon[^/]*(?:[?#]|$)|\\/fonts?\\//.test(path) ||
254
+ /^\\/(?:assets|@vite|__mol)\\//.test(path)
255
+ }
256
+ function shellRequest(r) {
257
+ if (r.type === 'document') return false
258
+ return SHELL_TYPE[r.type] === 1 || shellPath(r.path)
259
+ }
247
260
  // A failed request: a 4xx/5xx, or no response at all WITH Playwright's error text (a refused
248
- // connection). No response and no text is an aborted/pending request, not a failure.
261
+ // connection). No response and no text is an aborted/pending request, not a failure. A 404 on
262
+ // the page shell is excepted, for the reason above.
249
263
  function failedRequest(r) {
264
+ if (shellRequest(r) && r.status === 404) return false
250
265
  return r.status >= 400 || (r.status <= 0 && typeof r.failure === 'string')
251
266
  }
252
267
  const failing = []
@@ -254,6 +269,8 @@ for (const one of found.slice(0, MAX_TRACES)) {
254
269
  const rest = []
255
270
  for (let i = 0; i < requests.length; i++) {
256
271
  const r = requests[i]
272
+ // An answered/aborted shell request is no evidence at all — do not spend a kept row on it.
273
+ if (shellRequest(r) && !failedRequest(r)) continue
257
274
  if (failedRequest(r)) failing.push(i)
258
275
  else if (r.type === 'fetch' || r.type === 'xhr' || r.type === 'document') api.push(i)
259
276
  else rest.push(i)
@@ -277,6 +294,7 @@ for (const one of found.slice(0, MAX_TRACES)) {
277
294
  failure: r.failure,
278
295
  sent: r.sent,
279
296
  got: r.got,
297
+ type: r.type || undefined,
280
298
  })
281
299
  }
282
300
  // The page's own actions, timed. before/after pair on the call id, and only the LIBRARY
@@ -365,6 +383,7 @@ export function parseTraceArtifacts(stdout) {
365
383
  ...(typeof r.failure === 'string' && r.failure ? { failure: r.failure } : {}),
366
384
  ...(typeof r.sent === 'string' && r.sent ? { sent: r.sent } : {}),
367
385
  ...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
386
+ ...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
368
387
  })),
369
388
  console: consoleMessages.map(String),
370
389
  actions: actions
@@ -442,15 +461,77 @@ function wrappedInData(text) {
442
461
  return /^\{\s*"data"\s*:/.test(text.trim());
443
462
  }
444
463
  }
445
- /** Whether a trace recorded a request the server actually failed: a 4xx/5xx, or a network error. */
446
- function failedRequest(request) {
464
+ /**
465
+ * A request the page made for its own shell rather than for the step under test: the PWA's
466
+ * service worker (`/sw.js` — the scaffold registers one there and a project without one answers
467
+ * 404), the manifest, a favicon, the fonts a stylesheet pulls, and Vite's/dev's own asset paths.
468
+ *
469
+ * These are not the failing step's dependencies, so neither a missing response nor a 404 on one is
470
+ * a cause. xr-molecule-flash-m125 (+29:33) was handed "no response recorded — still in flight when
471
+ * the test ended … GET /sw.js; GET /fonts/ionicons.woff" and answered it by writing a CACHING
472
+ * service worker into the app — in front of `/assets/*`, exactly what makes a redeployed fix
473
+ * invisible to the next proof run. The note is about the test's own API calls.
474
+ */
475
+ const STATIC_ASSET_PATH = /^\/(?:sw|service-worker)\.js(?:[?#]|$)|\/manifest\.webmanifest(?:[?#]|$)|\/favicon[^/]*(?:[?#]|$)|\/fonts?\//i;
476
+ /** The build/dev output paths the page shell loads (Vite's bundle, the platform's own files). */
477
+ const STATIC_ASSET_DIR = /^\/(?:assets|@vite|__mol)\//i;
478
+ /** The resource types a page loads for itself rather than for the step under test. */
479
+ const STATIC_ASSET_TYPES = new Set(['font', 'image', 'stylesheet', 'script', 'manifest']);
480
+ /** The path part of a request, for the static-asset test. */
481
+ function requestPath(request) {
482
+ const url = String(request.path ?? '');
483
+ try {
484
+ const parsed = new URL(url, 'http://trace.local');
485
+ return parsed.pathname;
486
+ }
487
+ catch (_error) {
488
+ // Not a URL this can parse: judge the text itself, up to any query or fragment.
489
+ return url.split(/[?#]/)[0] ?? url;
490
+ }
491
+ }
492
+ /**
493
+ * Whether a request is one of the page shell's, not the test's (`STATIC_ASSET_PATH`).
494
+ *
495
+ * The document itself is never incidental — a page that never loaded IS the failure — and a
496
+ * request whose type is the app's own API (`fetch`/`xhr`) is judged by its path alone.
497
+ *
498
+ * @param request - One request a trace recorded.
499
+ * @returns True when the request is incidental to the step under test.
500
+ */
501
+ export function isStaticAssetRequest(request) {
502
+ if (request.type === 'document')
503
+ return false;
504
+ if (request.type !== undefined && STATIC_ASSET_TYPES.has(request.type))
505
+ return true;
506
+ const path = requestPath(request);
507
+ return STATIC_ASSET_PATH.test(path) || STATIC_ASSET_DIR.test(path);
508
+ }
509
+ /**
510
+ * Whether a trace recorded a request the server actually failed: a 4xx/5xx, or a network error.
511
+ *
512
+ * A 404 on a static asset is excepted: `/sw.js` and `/fonts/*` 404 on an app that never shipped
513
+ * them, and naming one as a failure sent m125 into writing the caching worker it had asked about.
514
+ * A 5xx on an asset, or a network error, still counts — the server did fail.
515
+ *
516
+ * @param request - One request a trace recorded.
517
+ * @returns True when the request failed in a way the note should name.
518
+ */
519
+ export function failedRequest(request) {
520
+ if (isStaticAssetRequest(request) && request.status === 404)
521
+ return false;
447
522
  return request.status >= 400 || (request.status <= 0 && request.failure !== undefined);
448
523
  }
449
524
  /**
450
525
  * Whether a trace recorded NO response and no reason for it — a request that was aborted or still
451
- * in flight when the test ended. It is not evidence that the server failed (m112 +30:45).
526
+ * in flight when the test ended. It is not evidence that the server failed (m112 +30:45), and one
527
+ * the page shell made is not evidence about the step at all (m125 +29:33).
528
+ *
529
+ * @param request - One request a trace recorded.
530
+ * @returns True when the request is still in flight and worth naming.
452
531
  */
453
- function unansweredRequest(request) {
532
+ export function unansweredRequest(request) {
533
+ if (isStaticAssetRequest(request))
534
+ return false;
454
535
  return request.status <= 0 && request.failure === undefined;
455
536
  }
456
537
  /**
@@ -508,9 +589,16 @@ function repeatsEarlierFailure(item, earlier) {
508
589
  * not evidence (m112 +53:46): four of the note's five rows were traces of one `afterEach` line
509
590
  * whose every request was `-1`, so the favorite cause — repeated in all three walks — stayed
510
591
  * hidden in "… and 7 more traced failures".
592
+ *
593
+ * A request the page shell made (m125 +29:33: `/sw.js`, a font) is not evidence about the step
594
+ * either, answered or not.
595
+ *
596
+ * @param item - One traced failure.
597
+ * @returns True when the trace holds a request or console error worth a row.
511
598
  */
512
599
  function traceRowEvidence(item) {
513
- return item.console.length > 0 || item.requests.some((request) => !unansweredRequest(request));
600
+ return (item.console.length > 0 ||
601
+ item.requests.some((request) => !isStaticAssetRequest(request) && !unansweredRequest(request)));
514
602
  }
515
603
  /**
516
604
  * The note's row order: a row with a REAL failure first, then a row whose failure an earlier walk
@@ -525,6 +613,51 @@ function traceRowTier(item, earlier) {
525
613
  return 1;
526
614
  return 2;
527
615
  }
616
+ /** A browser-level transport failure Chromium and Firefox both record (`net::ERR_*`). */
617
+ const NETWORK_ERROR = /\bnet::ERR_[A-Z0-9_]+\b/;
618
+ /**
619
+ * Whether the trace's page never finished a navigation: its own `Frame.goto` has no end time.
620
+ *
621
+ * A page that never loaded is not a set of app failures: a `page.goto` timeout on a document that
622
+ * ended in a `net::ERR_*` is the one line that explains every test that timed out on it, and it is
623
+ * what m118's preview walk never saw (`net::ERR_CONTENT_DECODING_FAILED` on a gzipped HTML body).
624
+ *
625
+ * @param item - One traced failure.
626
+ * @returns True when the page's navigation started and never ended.
627
+ */
628
+ function navigationNeverFinished(item) {
629
+ return item.actions?.some((action) => /\.goto$/.test(action.api) && action.end === null) ?? false;
630
+ }
631
+ /**
632
+ * The `net::ERR_*` the traced pages recorded, with the request it hit — or null.
633
+ *
634
+ * The request's own `_failureText` is preferred (Playwright records the browser's error there);
635
+ * a console error is the fallback, because the browser logs the same failure as
636
+ * `Failed to load resource: net::ERR_…` and a page that timed out on its document has no request
637
+ * failure text at all.
638
+ *
639
+ * @param items - The traces the note is about.
640
+ * @returns The first transport failure, or null when the browser reported none.
641
+ */
642
+ function transportFailure(items) {
643
+ for (const item of items) {
644
+ const document = item.requests.find((request) => request.type === 'document');
645
+ const failed = item.requests.find((request) => request.failure !== undefined && NETWORK_ERROR.test(request.failure));
646
+ if (failed !== undefined) {
647
+ const error = NETWORK_ERROR.exec(failed.failure ?? '')?.[0] ?? failed.failure ?? '';
648
+ return { error, where: failed.path };
649
+ }
650
+ for (const line of item.console) {
651
+ const error = NETWORK_ERROR.exec(line)?.[0];
652
+ if (error !== undefined)
653
+ return {
654
+ error,
655
+ where: document?.path ?? item.requests[0]?.path ?? 'the page',
656
+ };
657
+ }
658
+ }
659
+ return null;
660
+ }
528
661
  /**
529
662
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
530
663
  * what came back.
@@ -552,6 +685,10 @@ function traceRowTier(item, earlier) {
552
685
  * traces of one `afterEach` line whose every request was `-1`, and left the favorite cause — the
553
686
  * one failure that repeated in every walk — in its "… and N more".
554
687
  *
688
+ * A run whose every traced page never finished its navigation leads with the browser's own
689
+ * transport error (`net::ERR_CONTENT_DECODING_FAILED` and friends) and the document it hit: nothing
690
+ * rendered, so no row below is app evidence (xr-molecule-flash-m118 +37:09).
691
+ *
555
692
  * @param items - The traces {@link parseTraceArtifacts} read.
556
693
  * @param target - Where the run went — the deployed host, as the note names it.
557
694
  * @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
@@ -585,9 +722,12 @@ export function failureTraceNote(items, target, earlierFailures = []) {
585
722
  const anyFailed = traced.some((item) => item.requests.some(failedRequest));
586
723
  const rows = [];
587
724
  for (const [index, item] of traced.entries()) {
588
- const failed = item.requests.filter(failedRequest);
589
- const unanswered = item.requests.filter(unansweredRequest);
590
- const answered = item.requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
725
+ // The page shell's own requests take no row, in any bucket: an aborted `/sw.js` would
726
+ // otherwise be printed as an "answered" request with no response (m125 +29:33).
727
+ const requests = item.requests.filter((request) => !isStaticAssetRequest(request));
728
+ const failed = requests.filter(failedRequest);
729
+ const unanswered = requests.filter(unansweredRequest);
730
+ const answered = requests.filter((request) => !failedRequest(request) && !unansweredRequest(request));
591
731
  let room = MAX_TRACE_NOTE_REQUESTS;
592
732
  const parts = [];
593
733
  if (failed.length > 0) {
@@ -617,7 +757,26 @@ export function failureTraceNote(items, target, earlierFailures = []) {
617
757
  const more = distinct.size > traced.length
618
758
  ? `\n … and ${distinct.size - traced.length} more traced failures`
619
759
  : '';
620
- return (`The platform read the failing tests' own Playwright traces from the run against ${target} ` +
760
+ // A run whose pages never loaded has one cause, and it is not in the app: every traced failure is
761
+ // a `page.goto` that timed out on a document the browser could not load. The transport error is
762
+ // the first line the executor needs — m118 (+37:09) read 14 goto timeouts as app failures while
763
+ // the trace held `net::ERR_CONTENT_DECODING_FAILED` on every one.
764
+ const transport = candidates.length > 0 && candidates.every(navigationNeverFinished)
765
+ ? transportFailure(candidates)
766
+ : null;
767
+ const transportLead = transport === null
768
+ ? ''
769
+ : `Every failure this run traced is the same browser TRANSPORT failure: \`page.goto\` timed out ` +
770
+ `because the document at ${transport.where} ended \`${transport.error}\` — the page never ` +
771
+ `loaded, so none of these failures is the app's code or a selector. ` +
772
+ (transport.error === 'net::ERR_CONTENT_DECODING_FAILED'
773
+ ? 'A body whose gzip `content-encoding` no longer matches its bytes does this; a ' +
774
+ '`vite preview` over a build must be started with ' +
775
+ '`MOL_E2E_PREVIEW_PLUGIN=0 npx vite preview --port 4173 --strictPort`. '
776
+ : '') +
777
+ 'Fix the server before the app.\n\n';
778
+ return (transportLead +
779
+ `The platform read the failing tests' own Playwright traces from the run against ${target} ` +
621
780
  `(what each page sent and got: the requests that failed, the ones that got no response, and ` +
622
781
  `its last answered ones):\n` +
623
782
  `${rows.join('\n')}${more}\n` +