@molecule/api-ai-tools 1.14.3 → 1.14.5
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 +243 -189
- package/dist/browser-probe.d.ts +4 -4
- package/dist/browser-probe.d.ts.map +1 -1
- package/dist/browser-probe.js +5 -5
- package/dist/command-position.d.ts +15 -16
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +21 -23
- package/dist/cut-run.d.ts +9 -9
- package/dist/cut-run.js +6 -6
- package/dist/failure-kind.d.ts +6 -6
- package/dist/failure-kind.js +6 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/inline-script.d.ts +5 -5
- package/dist/inline-script.js +6 -6
- package/dist/line-diff.d.ts +2 -3
- package/dist/line-diff.d.ts.map +1 -1
- package/dist/line-diff.js +2 -3
- package/dist/no-test-run.d.ts +5 -5
- package/dist/no-test-run.d.ts.map +1 -1
- package/dist/no-test-run.js +9 -8
- package/dist/output-filter.d.ts +2 -2
- package/dist/output-filter.js +2 -2
- package/dist/pipeline-exit-note.d.ts +1 -1
- package/dist/pipeline-exit-note.js +1 -1
- package/dist/playwright-report.d.ts +1 -1
- package/dist/playwright-report.js +1 -1
- package/dist/server-command.d.ts +2 -2
- package/dist/server-command.js +2 -2
- package/dist/sleep-peek.d.ts +1 -1
- package/dist/sleep-peek.js +1 -1
- package/dist/suite-overlap.d.ts +24 -25
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +41 -43
- package/dist/tools.d.ts +26 -28
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +114 -72
- package/dist/trace-artifacts.d.ts +73 -65
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +76 -77
- package/dist/types.d.ts +5 -5
- package/dist/utilities.d.ts +128 -92
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +298 -143
- package/package.json +2 -2
package/dist/utilities.js
CHANGED
|
@@ -455,7 +455,7 @@ export function outputIsWithheldUntilExit(command) {
|
|
|
455
455
|
* The file a command sends its own output to, when it redirects it (`> out.txt`,
|
|
456
456
|
* `>> log`, `&> run.log`, `| tee [-a] file`), or null. A background command that does
|
|
457
457
|
* this leaves its task log empty, so the log is the wrong place to look and an empty
|
|
458
|
-
* log proves nothing
|
|
458
|
+
* log proves nothing.
|
|
459
459
|
*
|
|
460
460
|
* Which redirect counts is decided by the command's own shell structure, from the same
|
|
461
461
|
* parse every other command reader uses: a redirect written in a BACKGROUNDED statement
|
|
@@ -463,7 +463,7 @@ export function outputIsWithheldUntilExit(command) {
|
|
|
463
463
|
* the command's. The platform's own preview launch is exactly that — the preview server is
|
|
464
464
|
* started with `>/tmp/mol-preview-4173.log 2>&1 &` inside the build/serve prelude, and the
|
|
465
465
|
* suite's foreground command follows it — so a reader that took the last `>` in the whole
|
|
466
|
-
* text
|
|
466
|
+
* text tells a run its output went to the PREVIEW's log.
|
|
467
467
|
*
|
|
468
468
|
* The target comes back without its shell quotes: `> '/tmp/x.log'` is `/tmp/x.log`, and
|
|
469
469
|
* the callers resolve a relative one against the command's cwd.
|
|
@@ -531,8 +531,8 @@ const PRINTING_REPORTERS = new Set(['list', 'line', 'dot', 'basic', 'spec']);
|
|
|
531
531
|
* Every background command is given this (see {@link streamingBackgroundCommand}), so a suite the
|
|
532
532
|
* executor starts through its own wrapper is covered too — the wrapper passes the environment on
|
|
533
533
|
* to the Playwright process it spawns. Without it the log's only failure lines are `✘` rows:
|
|
534
|
-
*
|
|
535
|
-
*
|
|
534
|
+
* A run told to `grep -n -A 20 '<test name>'` the log got nothing back through four greps,
|
|
535
|
+
* inferred causes from the spec prose instead, and
|
|
536
536
|
* edited `app/src` while the run was still walking — which voids the run.
|
|
537
537
|
*/
|
|
538
538
|
export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE';
|
|
@@ -560,7 +560,7 @@ const STATUS_EXIT_ARITHMETIC = /^[A-Za-z0-9_|&+\-*/%^<>=!~() \t]*$/;
|
|
|
560
560
|
* `npx playwright test … > /tmp/pw.log 2>&1; echo EXIT $?` — and reading the split's last part as
|
|
561
561
|
* the run withheld the reporter, the inline-failure switch and `--retries=0` from every one of
|
|
562
562
|
* them: the log then carried `✘` rows and nothing else while the wait note promised each failure's
|
|
563
|
-
* error under them
|
|
563
|
+
* error under them. Whitespace and a group's closing
|
|
564
564
|
* punctuation are not part of the step; {@link streamingBackgroundCommand} strips those before
|
|
565
565
|
* asking, and strips them here too for the split it is given.
|
|
566
566
|
*
|
|
@@ -570,7 +570,7 @@ const STATUS_EXIT_ARITHMETIC = /^[A-Za-z0-9_|&+\-*/%^<>=!~() \t]*$/;
|
|
|
570
570
|
* browser half as a status step is what lets the run keep its reporter, its machine report and the
|
|
571
571
|
* inline-failure switch — without it, every platform proof ran its browser suite on the request's
|
|
572
572
|
* own `line` reporter, which writes no ✓/✘ mark and no per-failure error into the log the wait
|
|
573
|
-
* reads
|
|
573
|
+
* reads.
|
|
574
574
|
*
|
|
575
575
|
* Only the two shapes whose whole value is the shell's own status are read this way; anything that
|
|
576
576
|
* could run — a command substitution, a backticked word, a suffix after the status — stays a step.
|
|
@@ -596,7 +596,7 @@ function isStatusOnlyStep(step) {
|
|
|
596
596
|
* Rewrite a command about to run in the background so its log grows while it
|
|
597
597
|
* runs. Two shapes left the log empty until exit, and an empty log is what
|
|
598
598
|
* `wait_for_task` reads as "stuck", so working suites were reported stuck and
|
|
599
|
-
* killed (
|
|
599
|
+
* killed (one run lost 20 of its 60 minutes this way):
|
|
600
600
|
*
|
|
601
601
|
* - a trailing `| tail -N`, which holds everything back until the command ends.
|
|
602
602
|
* It is dropped: `wait_for_task` already returns the end of the log.
|
|
@@ -604,7 +604,7 @@ function isStatusOnlyStep(step) {
|
|
|
604
604
|
* failing test's own error and leaves the pipeline's exit status — the filter's — as the run's.
|
|
605
605
|
* Every such stage is dropped from EVERY suite run in the command, not only the last
|
|
606
606
|
* ({@link stripTrailingSuiteFilters}); `wait_for_task` already returns the end of the full log,
|
|
607
|
-
* and a run whose tally shows a failure must never report 0
|
|
607
|
+
* and a run whose tally shows a failure must never report 0.
|
|
608
608
|
* - `playwright test` with no `--reporter`, which runs the config's reporter —
|
|
609
609
|
* often `html`, which prints nothing per test. `--reporter=list` is added
|
|
610
610
|
* when the playwright run is the command's last step, piped or not: it writes each result as
|
|
@@ -614,11 +614,11 @@ function isStatusOnlyStep(step) {
|
|
|
614
614
|
*
|
|
615
615
|
* The run ends up with exactly ONE reporter that prints each failure — `list`. A run that names
|
|
616
616
|
* any other printing reporter has it REPLACED, never joined: two of them write every test result
|
|
617
|
-
* into the log twice, which
|
|
617
|
+
* into the log twice, which made a `--reporter=list,line` run (its
|
|
618
618
|
* `line` added by this function one run earlier) report two distinct failures as "4 of this run's
|
|
619
619
|
* tests have already failed the same way", and the executor stopped a 139-test walk at test 21
|
|
620
620
|
* with a single cause in hand. `line` alone is no use either: it writes `[n/N] <title>` with no
|
|
621
|
-
* ✓/✘ mark and no duration, so
|
|
621
|
+
* ✓/✘ mark and no duration, so an 8.3-minute walk of one suite could never trip
|
|
622
622
|
* the fail-fast note. A reporter set that writes a machine format to a file (`json`, `html`,
|
|
623
623
|
* `junit`) is the
|
|
624
624
|
* executor's own and is left exactly as it is — so is any reporter this build does not know.
|
|
@@ -627,8 +627,8 @@ function isStatusOnlyStep(step) {
|
|
|
627
627
|
* {@link PLAYWRIGHT_FAILURES_INLINE_ENV} (inert except for Playwright's `list` reporter), so each
|
|
628
628
|
* failure's own error is in the log as that test fails rather than only at the end: a suite that is
|
|
629
629
|
* still walking is exactly when the executor reads the log, and without it a running log carries
|
|
630
|
-
* `✘` rows and nothing else (
|
|
631
|
-
* guessed;
|
|
630
|
+
* `✘` rows and nothing else (~90 s went to greps of one, then to fixing what was
|
|
631
|
+
* guessed; a wrapper-launched run lost 6.5 minutes to the same shape).
|
|
632
632
|
*
|
|
633
633
|
* Only the step that runs the suite is touched, and only its reporter, and only when that step
|
|
634
634
|
* runs something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
@@ -639,9 +639,8 @@ function isStatusOnlyStep(step) {
|
|
|
639
639
|
*
|
|
640
640
|
* A command that starts the suite MORE THAN ONCE is not launched at all: every run it carries is
|
|
641
641
|
* a whole walk of the same suite against the same app, and only the last step is rewritten, so the
|
|
642
|
-
* earlier walks run unmeasured for nothing.
|
|
643
|
-
*
|
|
644
|
-
* first had not. The platform refuses a second CONCURRENT run against one app and database by the
|
|
642
|
+
* earlier walks run unmeasured for nothing. Two whole walks of one 138-test suite chained with
|
|
643
|
+
* `;` — 4.5 minutes each — measured nothing the first had not. The platform refuses a second CONCURRENT run against one app and database by the
|
|
645
644
|
* ONE rule ({@link suiteRunInTheWay}) — for every pair, whatever uid a run mints — and this is the
|
|
646
645
|
* same hazard inside one command ({@link suiteRunStatements}). `error` then carries the refusal,
|
|
647
646
|
* naming each suite step and the ONE command to send instead (the last suite step, rewritten as
|
|
@@ -652,8 +651,8 @@ function isStatusOnlyStep(step) {
|
|
|
652
651
|
* excludes by name, and whether this command IS the platform's graded proof
|
|
653
652
|
* ({@link DiagnosisRunContext}). A `--grep-invert` of exactly those exclusions is the
|
|
654
653
|
* request's own scope, not a diagnosis, so it keeps the suite's retries on a deployed proof
|
|
655
|
-
*
|
|
656
|
-
* keeps them whatever its scope, targeted set included
|
|
654
|
+
* — and a proof the platform itself composed and launched
|
|
655
|
+
* keeps them whatever its scope, targeted set included.
|
|
657
656
|
* @param reportPath - Where this run should write Playwright's standard `json` report, when the
|
|
658
657
|
* caller keeps one ({@link ./playwright-report.js}). The reporter is added to the run's own
|
|
659
658
|
* reporter set, never in place of one, and its output goes to that file through
|
|
@@ -755,8 +754,8 @@ export function streamingBackgroundCommand(command, context, reportPath) {
|
|
|
755
754
|
* (`| tee run.log`), and a statement can end on a group closer (`( … )`, `{ …; }`) or a
|
|
756
755
|
* redirection (`… 2>&1`). Appending after the `)` of a subshell was a shell syntax error that
|
|
757
756
|
* killed the run at once (`sh: Syntax error: word unexpected`) and pushed the model off the
|
|
758
|
-
* platform's runner shape
|
|
759
|
-
*
|
|
757
|
+
* platform's runner shape; appending after a pipe hands the word to the filter (a dangling
|
|
758
|
+
* `--trace` flag, in the same shape).
|
|
760
759
|
*
|
|
761
760
|
* The suite's own segment is found wherever it sits ({@link lastSuiteRun}), because a trailing
|
|
762
761
|
* status step is not another command's output: in `… > /tmp/pw.log 2>&1; echo EXIT $?` the run
|
|
@@ -928,7 +927,7 @@ export function streamingBackgroundCommand(command, context, reportPath) {
|
|
|
928
927
|
* reporter.
|
|
929
928
|
*
|
|
930
929
|
* A compound command's earlier walk is not a preview: it writes the same log the executor reads,
|
|
931
|
-
* and
|
|
930
|
+
* and there the first 3.6-minute walk keeps its own
|
|
932
931
|
* `--reporter=line`, which prints `[n/N] <title>` rows with no ✓/✘ mark and no per-failure error.
|
|
933
932
|
* What came back was `72 passed` and two failure NAMES; the error bodies existed only in the walk's
|
|
934
933
|
* discarded log, and diagnosing them needed another run.
|
|
@@ -1137,7 +1136,7 @@ export function isSuiteRunCommand(command) {
|
|
|
1137
1136
|
* Refuse only a command made entirely of identical suite invocations.
|
|
1138
1137
|
*
|
|
1139
1138
|
* Mixed shell commands execute as written. Rebuilding their prefix or suffix can lose writes,
|
|
1140
|
-
* break heredoc delimiters or turn an && guard into unconditional work
|
|
1139
|
+
* break heredoc delimiters or turn an && guard into unconditional work. Different args
|
|
1141
1140
|
* or environments may select different suites or targets; they are not known duplicates either.
|
|
1142
1141
|
* This is an optimization, so uncertainty always allows the original command.
|
|
1143
1142
|
*
|
|
@@ -1197,8 +1196,8 @@ function suiteRunnerArguments(command) {
|
|
|
1197
1196
|
* flags, and the flags that could not be placed.
|
|
1198
1197
|
*
|
|
1199
1198
|
* The grader walks the suite under its OWN budget, so a run whose runner config sets a different
|
|
1200
|
-
* timeout or worker count measures a run the grader never sees (
|
|
1201
|
-
*
|
|
1199
|
+
* timeout or worker count measures a run the grader never sees (a 60 s wrapper config produced
|
|
1200
|
+
* a green local proof the official 15 s grader then failed). Playwright
|
|
1202
1201
|
* lets a command-line flag win over every config file, so the suite's values are put on the
|
|
1203
1202
|
* runner's own arguments — written where {@link statementArgumentEnd} places them, never after a
|
|
1204
1203
|
* pipe, redirect or group closer. Only the command's LAST suite step is touched: that is the run
|
|
@@ -1312,7 +1311,7 @@ export function suiteRunConfigCommand(command, config) {
|
|
|
1312
1311
|
* Used where the platform moves a run to the background: the executor wrote the bound to fit this
|
|
1313
1312
|
* tool's foreground ceiling, and in the background that ceiling no longer applies — the task is
|
|
1314
1313
|
* bounded by `wait_for_task` and the turn's deadline — so the prefix only kills the walk
|
|
1315
|
-
* ({@link withoutTimeoutWrapper}
|
|
1314
|
+
* ({@link withoutTimeoutWrapper}).
|
|
1316
1315
|
*
|
|
1317
1316
|
* @param command - The shell command.
|
|
1318
1317
|
* @returns The rewritten command, and the largest lifted bound (null when nothing was lifted).
|
|
@@ -1325,7 +1324,7 @@ export function withoutSuiteTimeout(command) {
|
|
|
1325
1324
|
* that runs something.
|
|
1326
1325
|
*
|
|
1327
1326
|
* `npx playwright test --list` matched every `\bplaywright\s+test\b` test in the platform and was
|
|
1328
|
-
* then treated as a run: held (
|
|
1327
|
+
* then treated as a run: held (a config check), backgrounded for a run that outlasts
|
|
1329
1328
|
* the tool's ceiling, and rewritten with `--reporter`. A list takes two seconds, prints its lines
|
|
1330
1329
|
* and exits, so it is none of those; {@link commandRunsNoTests} is what decides.
|
|
1331
1330
|
*
|
|
@@ -1378,8 +1377,7 @@ const OUTPUT_REDIRECT = /^(?:\d*|&)>>?(?!&)/;
|
|
|
1378
1377
|
* searching the command for the re-joined text: the words are joined back with single spaces for
|
|
1379
1378
|
* matching, so `command.indexOf(segment.text)` comes up empty on irregular spacing, on a grouping
|
|
1380
1379
|
* brace, or on anything a heredoc body shifts — and the caller then cut at a guess or refused to
|
|
1381
|
-
* cut at all (
|
|
1382
|
-
* "piped" for the rest of the session).
|
|
1380
|
+
* cut at all (a heredoc body turned a finished run into "piped" for the rest of the session).
|
|
1383
1381
|
*
|
|
1384
1382
|
* A statement inside a nested `sh -c '<command>'` string is listed with null offsets: its
|
|
1385
1383
|
* positions are positions in THAT string, so nothing may be spliced at them — but its separator
|
|
@@ -1456,7 +1454,7 @@ function lastSuitePipeline(command) {
|
|
|
1456
1454
|
* the command exits with is then the following statement's. That is the right answer to "whose
|
|
1457
1455
|
* exit code is this", and the wrong one to "which stages filter this run's output": a
|
|
1458
1456
|
* `… --reporter=list 2>&1 | tail -5; echo done` still pipes its log through a tail, and the tail
|
|
1459
|
-
* still throws away every failure line the run printed (
|
|
1457
|
+
* still throws away every failure line the run printed (where the
|
|
1460
1458
|
* platform's own hold handed that shape back and the first 95-second walk's log held only the
|
|
1461
1459
|
* tally).
|
|
1462
1460
|
*
|
|
@@ -1473,8 +1471,8 @@ function lastSuiteRun(command) {
|
|
|
1473
1471
|
* spec, or a Hurl run followed by a browser walk — and every one of them writes the log the
|
|
1474
1472
|
* executor reads. Reading only the last one left the earlier walk's failing test's own error
|
|
1475
1473
|
* behind a `| tail -5` while the LAST invocation was rewritten normally
|
|
1476
|
-
* (
|
|
1477
|
-
*
|
|
1474
|
+
* (a 3.6-minute walk returned `72 passed` with no error body when the driver ran
|
|
1475
|
+
* `… specs 2>&1 | tail -5; echo START; … auth.spec.ts`).
|
|
1478
1476
|
*
|
|
1479
1477
|
* A command that runs no test at all is not a set of runs ({@link commandRunsNoTests}); nor is a
|
|
1480
1478
|
* statement that only lists tests, which the reporter pass asks about per statement
|
|
@@ -1566,13 +1564,13 @@ function suiteFilterCut(command, located, at) {
|
|
|
1566
1564
|
*
|
|
1567
1565
|
* A filter after a runner is not "the end of the log" the executor asked for: it hides the
|
|
1568
1566
|
* failing test's own error (it usually matches none of `✘|passed|failed`), and the pipeline's exit
|
|
1569
|
-
* status becomes the FILTER's, so `… 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20`
|
|
1570
|
-
* `exitCode: 0` for a run with a failure in it
|
|
1567
|
+
* status becomes the FILTER's, so `… 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20` comes
|
|
1568
|
+
* back `exitCode: 0` for a run with a failure in it. The tool returns
|
|
1571
1569
|
* the end of the full log anyway, so the filter only costs diagnosability.
|
|
1572
1570
|
*
|
|
1573
1571
|
* EVERY run is stripped, not only the command's last: a compound command's earlier walk writes
|
|
1574
1572
|
* the same log, and its `| tail -5` threw away the error body of a 3.6-minute Playwright walk
|
|
1575
|
-
* whose two failure names were all the executor got back
|
|
1573
|
+
* whose two failure names were all the executor got back. Only a
|
|
1576
1574
|
* run the parse places is touched, and only its own pipeline — a later statement's filters belong
|
|
1577
1575
|
* to it.
|
|
1578
1576
|
*
|
|
@@ -1583,12 +1581,12 @@ function suiteFilterCut(command, located, at) {
|
|
|
1583
1581
|
*
|
|
1584
1582
|
* A statement after the run's own pipeline (`; echo done`, `&& echo ok`) does not protect the
|
|
1585
1583
|
* filters before it: the filter is thrown away and the trailing statement is kept, so the log the
|
|
1586
|
-
* run writes still holds every failure (
|
|
1584
|
+
* run writes still holds every failure (a `| tail -5; echo done` keeps its
|
|
1587
1585
|
* tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
|
|
1588
1586
|
* that command back).
|
|
1589
1587
|
*
|
|
1590
1588
|
* The command around the dropped stages is left as written: a group they sit in keeps its closer
|
|
1591
|
-
* (`( … | tail -25)` → `( … )`, never `( …`
|
|
1589
|
+
* (`( … | tail -25)` → `( … )`, never `( …`), and a stage the cut
|
|
1592
1590
|
* would leave a closer dangling inside is not dropped at all.
|
|
1593
1591
|
*
|
|
1594
1592
|
* @param command - The shell command.
|
|
@@ -1630,8 +1628,8 @@ export function stripTrailingSuiteFilters(command) {
|
|
|
1630
1628
|
* code a finished run reports is its LAST pipeline stage's, so a stage after the runner makes the
|
|
1631
1629
|
* code the stage's — but only when such a stage EXISTS. The platform used to report "piped" for
|
|
1632
1630
|
* any run whose statements it could not place in the text, which one heredoc body was enough to
|
|
1633
|
-
* cause:
|
|
1634
|
-
*
|
|
1631
|
+
* cause: a config written in the same `exec_command` made the run come back with no exit code
|
|
1632
|
+
* at all, and the note under it claimed a stage after the
|
|
1635
1633
|
* runner that was not there. The parse now places every statement (`locatedCommandSegments`), so
|
|
1636
1634
|
* "unplaced" is the rare, named case rather than a guess dressed as an answer.
|
|
1637
1635
|
*
|
|
@@ -1673,10 +1671,9 @@ export function suiteRunPipedAfterRunner(command) {
|
|
|
1673
1671
|
* shares with everything else stays responsive to what the executor does next.
|
|
1674
1672
|
*
|
|
1675
1673
|
* A whole-suite Playwright run saturates the one container the app's Vite
|
|
1676
|
-
* server, its API process and every other command also live in:
|
|
1677
|
-
* the
|
|
1678
|
-
*
|
|
1679
|
-
* measured on the chat payload and on the live-stream follower independently.
|
|
1674
|
+
* server, its API process and every other command also live in: a backgrounded 139-test run
|
|
1675
|
+
* has left the two-word shell command sent next — tick its plan file — returning nothing for
|
|
1676
|
+
* 2 min 40 s, measured on the chat payload and on the live-stream follower independently.
|
|
1680
1677
|
* The backgrounding note is right that the executor should go do other work;
|
|
1681
1678
|
* what it cannot promise is that the sandbox will let it.
|
|
1682
1679
|
*
|
|
@@ -1824,8 +1821,8 @@ const BLOCKED_MOL_REDIRECT = /(?:<\s*\/etc\/mol)/i;
|
|
|
1824
1821
|
* The first check reads the commands the shell actually RUNS — the statements, a `sh -c`
|
|
1825
1822
|
* string's body, and every command substitution ({@link executedCommands}) — so a `|` inside a
|
|
1826
1823
|
* quoted grep alternation is data, not a pipeline: `grep -E "USER CALLS|ENV|BUTTON|passed|failed"
|
|
1827
|
-
* app.log` prints log lines, and the `ENV` in its pattern
|
|
1828
|
-
*
|
|
1824
|
+
* app.log` prints log lines, and the `ENV` in its pattern starts nothing — a text
|
|
1825
|
+
* match would block exactly that call as an environment dump, twice over. What the
|
|
1829
1826
|
* shell DOES run stays visible through the same walk: `ls | env`, `bash -c 'env'`,
|
|
1830
1827
|
* `echo "$(printenv)"` and a nested `sh -c 'x; env'` are all commands, and so is `env` itself
|
|
1831
1828
|
* when nothing follows it.
|
|
@@ -1870,21 +1867,30 @@ export function checkBlockedCommand(command) {
|
|
|
1870
1867
|
export function resolvePath(path, projectRoot) {
|
|
1871
1868
|
if (path === '' || path === '/')
|
|
1872
1869
|
return projectRoot;
|
|
1873
|
-
const
|
|
1874
|
-
const resolved = clean.startsWith('/')
|
|
1875
|
-
? posix.normalize(clean)
|
|
1876
|
-
: posix.normalize(`${projectRoot}/${clean}`);
|
|
1870
|
+
const resolved = rootedPath(path, projectRoot);
|
|
1877
1871
|
if (resolved !== projectRoot && !resolved.startsWith(projectRoot + '/'))
|
|
1878
1872
|
return projectRoot;
|
|
1879
1873
|
return resolved;
|
|
1880
1874
|
}
|
|
1875
|
+
/**
|
|
1876
|
+
* A tool path as an absolute posix path: an absolute path normalized, a relative
|
|
1877
|
+
* one resolved against the project root — the one rule {@link resolvePath},
|
|
1878
|
+
* {@link isOutsideRoot} and {@link commandStartsWithCdInto} all compare against.
|
|
1879
|
+
*
|
|
1880
|
+
* @param path - The path the tool was given.
|
|
1881
|
+
* @param projectRoot - The project root.
|
|
1882
|
+
* @returns The normalized absolute path.
|
|
1883
|
+
*/
|
|
1884
|
+
function rootedPath(path, projectRoot) {
|
|
1885
|
+
const clean = path.replace(/\0/g, '');
|
|
1886
|
+
return clean.startsWith('/') ? posix.normalize(clean) : posix.normalize(`${projectRoot}/${clean}`);
|
|
1887
|
+
}
|
|
1881
1888
|
/**
|
|
1882
1889
|
* Whether a tool path points outside the project root once normalized (an
|
|
1883
1890
|
* absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
|
|
1884
1891
|
* clamps such a path to the root; a file tool must refuse it instead of
|
|
1885
1892
|
* answering about the root — a `list_files /tmp/x` used to return the
|
|
1886
|
-
* workspace listing as if it were `/tmp/x
|
|
1887
|
-
* 2026-10-14).
|
|
1893
|
+
* workspace listing as if it were `/tmp/x`.
|
|
1888
1894
|
*
|
|
1889
1895
|
* @param path - The path the tool was given.
|
|
1890
1896
|
* @param projectRoot - The project root.
|
|
@@ -1893,12 +1899,60 @@ export function resolvePath(path, projectRoot) {
|
|
|
1893
1899
|
export function isOutsideRoot(path, projectRoot) {
|
|
1894
1900
|
if (path === '' || path === '/')
|
|
1895
1901
|
return false;
|
|
1896
|
-
const
|
|
1897
|
-
const resolved = clean.startsWith('/')
|
|
1898
|
-
? posix.normalize(clean)
|
|
1899
|
-
: posix.normalize(`${projectRoot}/${clean}`);
|
|
1902
|
+
const resolved = rootedPath(path, projectRoot);
|
|
1900
1903
|
return resolved !== projectRoot && !resolved.startsWith(projectRoot + '/');
|
|
1901
1904
|
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Whether a command's FIRST statement already changes into `directory`.
|
|
1907
|
+
*
|
|
1908
|
+
* `exec_command` refuses a `cwd` outside the workspace, and that refusal tells
|
|
1909
|
+
* the caller to drop `cwd` and start the command with `cd <path> && `. When the
|
|
1910
|
+
* command ALREADY starts that way the advice describes what was just sent, so
|
|
1911
|
+
* the command runs as written with the backend's working directory at the root
|
|
1912
|
+
* instead (`{cwd: "/tmp", command: "cd /tmp && …"}` was refused with "start the command with
|
|
1913
|
+
* `cd /tmp && `"; the same command resent with `cwd: "/workspace"` then passes).
|
|
1914
|
+
*
|
|
1915
|
+
* Only the leading statement counts, only when the shell's own parse puts it in
|
|
1916
|
+
* the caller's shell — a `( cd … )` group or a `sh -c '…'` runs in its own
|
|
1917
|
+
* directory and moves nothing — and only when it ends the way a following
|
|
1918
|
+
* command needs (`&&`, `;`, a newline, or nothing after it). `cd … | cat` is a
|
|
1919
|
+
* pipeline, so its `cd` runs in a subshell, and `&` backgrounds it: neither
|
|
1920
|
+
* leaves the caller's directory changed.
|
|
1921
|
+
*
|
|
1922
|
+
* @param command - The shell command as the tool received it.
|
|
1923
|
+
* @param directory - The `cwd` the caller asked for.
|
|
1924
|
+
* @param projectRoot - The project root a relative path is resolved against.
|
|
1925
|
+
* @returns True when the command's first statement `cd`s into `directory`.
|
|
1926
|
+
*/
|
|
1927
|
+
export function commandStartsWithCdInto(command, directory, projectRoot) {
|
|
1928
|
+
const first = locatedCommandSegments(command)[0];
|
|
1929
|
+
if (!first || first.segment === null || first.segment.program !== 'cd')
|
|
1930
|
+
return false;
|
|
1931
|
+
if ((first.shellScope?.length ?? 0) > 0)
|
|
1932
|
+
return false;
|
|
1933
|
+
const terminator = first.terminator;
|
|
1934
|
+
if (terminator !== '&&' && terminator !== ';' && terminator !== '\n' && terminator !== '') {
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
if (first.segment.args.length !== 1)
|
|
1938
|
+
return false;
|
|
1939
|
+
const target = withoutQuotes(first.segment.args[0] ?? '');
|
|
1940
|
+
return rootedPath(target, projectRoot) === rootedPath(directory, projectRoot);
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* A shell word with one surrounding quote pair removed (`'/tmp'` → `/tmp`,
|
|
1944
|
+
* `"/tmp"` → `/tmp`). A word without a matching pair is returned as written.
|
|
1945
|
+
*
|
|
1946
|
+
* @param word - One word of a parsed statement, as written.
|
|
1947
|
+
* @returns The word's text.
|
|
1948
|
+
*/
|
|
1949
|
+
function withoutQuotes(word) {
|
|
1950
|
+
const quote = word[0];
|
|
1951
|
+
if ((quote === "'" || quote === '"') && word.length >= 2 && word.endsWith(quote)) {
|
|
1952
|
+
return word.slice(1, -1);
|
|
1953
|
+
}
|
|
1954
|
+
return word;
|
|
1955
|
+
}
|
|
1902
1956
|
/**
|
|
1903
1957
|
* Validate that a glob/include pattern is safe (no shell metacharacters that could
|
|
1904
1958
|
* inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
|
|
@@ -1944,8 +1998,8 @@ export const BRACE_GLOB_HINT = `or one brace group of up to ${MAX_BRACE_GLOB_ALT
|
|
|
1944
1998
|
* Neither search tool expands braces itself: `grep --include` and `find` take
|
|
1945
1999
|
* the pattern as a single shell-quoted argument, so no shell ever sees it.
|
|
1946
2000
|
* `find_files {pattern:"*.{ts,tsx}"}` and `search_files {include:"*.{ts,tsx}"}`
|
|
1947
|
-
* were both refused outright
|
|
1948
|
-
*
|
|
2001
|
+
* were both refused outright, costing a round trip and pushing the agent to a narrower
|
|
2002
|
+
* search. The group is expanded here
|
|
1949
2003
|
* instead — `search_files` passes one `--include` per alternative and
|
|
1950
2004
|
* `find_files` ORs one `-name`/`-path` predicate per alternative.
|
|
1951
2005
|
*
|
|
@@ -1985,8 +2039,8 @@ export function expandBraceGlob(pattern) {
|
|
|
1985
2039
|
*
|
|
1986
2040
|
* `find -name` compares a pattern against a file's BASENAME, so a pattern
|
|
1987
2041
|
* containing a slash (`helpers/*`, `e2e/*.spec.ts`, `src` + `**` + `/App.tsx`)
|
|
1988
|
-
* can never match and the tool answers `[]` (
|
|
1989
|
-
*
|
|
2042
|
+
* can never match and the tool answers `[]` (twice in one run). Such a pattern is matched
|
|
2043
|
+
* with `-path` instead: an fnmatch
|
|
1990
2044
|
* glob over the whole printed path, whose `*` crosses `/` — the rule
|
|
1991
2045
|
* `.gitignore` uses, and the reason a bare `helpers/*` means "a `helpers/`
|
|
1992
2046
|
* directory anywhere below the root". The pattern is anchored with a leading
|
|
@@ -2036,25 +2090,38 @@ export function findPathArgs(pattern) {
|
|
|
2036
2090
|
* search root. Handed `/workspace/node_modules` and
|
|
2037
2091
|
* `@molecule/api-resource-user/dist/*.d.ts` — a directory named outright —
|
|
2038
2092
|
* `find` walked the sandbox's entire dependency tree and hit the tool's 10 s
|
|
2039
|
-
* deadline without printing a result (
|
|
2040
|
-
*
|
|
2093
|
+
* deadline without printing a result (the one directory the model meant answered a `list_files`
|
|
2094
|
+
* in 0 s). The literal prefix is that
|
|
2041
2095
|
* directory, and rooting the search at it is what keeps the call instant.
|
|
2042
2096
|
*
|
|
2043
2097
|
* A BARE first segment (`helpers/*`, `bonds/*`, `e2e/*.spec.ts`) is NOT one:
|
|
2044
2098
|
* `helpers/*` means "a `helpers/` directory anywhere below the root", which the
|
|
2045
|
-
* tool documents and a live run relied on (
|
|
2046
|
-
*
|
|
2099
|
+
* tool documents and a live run relied on (a path pattern is not anchored to one depth).
|
|
2100
|
+
* Rooting those at
|
|
2047
2101
|
* `<root>/helpers` would answer with a SUBSET, silently: the nested
|
|
2048
2102
|
* `api/src/helpers/` matches would disappear, and a missing file reads as
|
|
2049
2103
|
* "no such file". So a prefix is returned only when it names a path of two or
|
|
2050
2104
|
* more segments (`@molecule/api-resource-user/dist`), which is the shape that
|
|
2051
2105
|
* is a directory rather than a directory name.
|
|
2052
2106
|
*
|
|
2107
|
+
* An npm SCOPE as the first segment, under a `node_modules` root, is the one
|
|
2108
|
+
* exception that parameter `root` enables: npm installs a scope only directly
|
|
2109
|
+
* inside a `node_modules` directory, and the walk already prunes every nested
|
|
2110
|
+
* `node_modules` (see `findPruneArgs`), so the whole-tree form can only ever
|
|
2111
|
+
* match `<root>/@molecule/…` — the scope directory IS the directory the pattern
|
|
2112
|
+
* names. Left un-narrowed, a scoped-package pattern walked the sandbox's entire
|
|
2113
|
+
* dependency tree and hit the 10 s deadline with no result at all. A bare NAME stays a
|
|
2114
|
+
* directory name at any depth:
|
|
2115
|
+
* `helpers/*` is unchanged even under a `node_modules` root.
|
|
2116
|
+
*
|
|
2053
2117
|
* @param pattern - A pattern already accepted by {@link isValidGlob}.
|
|
2118
|
+
* @param root - The resolved search root, when one is already known; a
|
|
2119
|
+
* scope-first prefix is returned only when this ends in a `node_modules`
|
|
2120
|
+
* segment (and the pattern has more path after the scope).
|
|
2054
2121
|
* @returns The literal directory prefix, without a trailing slash, or null when
|
|
2055
2122
|
* the pattern has no glob character, no `/`, or only a bare name before one.
|
|
2056
2123
|
*/
|
|
2057
|
-
export function literalDirPrefix(pattern) {
|
|
2124
|
+
export function literalDirPrefix(pattern, root) {
|
|
2058
2125
|
const stripped = pattern.replace(/^(?:\.?\/)+/, '');
|
|
2059
2126
|
const firstGlob = stripped.search(/[?*[]/);
|
|
2060
2127
|
// No glob at all: the pattern IS a path, and the whole-tree form already
|
|
@@ -2065,7 +2132,14 @@ export function literalDirPrefix(pattern) {
|
|
|
2065
2132
|
if (at <= 0)
|
|
2066
2133
|
return null;
|
|
2067
2134
|
const prefix = stripped.slice(0, at);
|
|
2068
|
-
|
|
2135
|
+
if (prefix.includes('/'))
|
|
2136
|
+
return prefix;
|
|
2137
|
+
// One segment: a bare directory NAME ("any depth", flash-m77) — except an npm
|
|
2138
|
+
// scope directly inside the node_modules root, which is one directory (above).
|
|
2139
|
+
if (root === undefined || !/^@[^/*?[]+$/.test(prefix))
|
|
2140
|
+
return null;
|
|
2141
|
+
const rootName = root.replace(/\/+$/, '').split('/').pop();
|
|
2142
|
+
return rootName === 'node_modules' ? prefix : null;
|
|
2069
2143
|
}
|
|
2070
2144
|
/**
|
|
2071
2145
|
* Whether a backend error is the sandbox's own command deadline.
|
|
@@ -2074,7 +2148,7 @@ export function literalDirPrefix(pattern) {
|
|
|
2074
2148
|
* `[deadline_exceeded] the operation timed out: … It can be modified by passing
|
|
2075
2149
|
* 'timeoutMs' when making the request. Use '0' to disable the timeout.` — which
|
|
2076
2150
|
* names an option the executor cannot set and says nothing about the search
|
|
2077
|
-
* that died. Handing that sentence to the model is what
|
|
2151
|
+
* that died. Handing that sentence to the model is what a run
|
|
2078
2152
|
* read after `find_files` spent 10 s walking `node_modules`; the tool answers
|
|
2079
2153
|
* the deadline in its own words instead (see {@link searchTimeoutError}).
|
|
2080
2154
|
*
|
|
@@ -2155,8 +2229,8 @@ export function pathArgError(path, tool) {
|
|
|
2155
2229
|
*
|
|
2156
2230
|
* The path message is the wrong reading of it: "requires a non-empty path
|
|
2157
2231
|
* argument" says the call was understood and the path was left out, so the model
|
|
2158
|
-
* retries the call it just made instead of sending the whole call again (
|
|
2159
|
-
*
|
|
2232
|
+
* retries the call it just made instead of sending the whole call again (`edit_file {}` loses
|
|
2233
|
+
* one round). An empty object and a
|
|
2160
2234
|
* missing `path` are different failures and deserve different sentences.
|
|
2161
2235
|
*
|
|
2162
2236
|
* @param input - The raw tool input.
|
|
@@ -2300,9 +2374,8 @@ export const MAX_BATCH_READ_FILES = 25;
|
|
|
2300
2374
|
* MIDDLE — the cut lands between two `"content"` values, so whole files fell
|
|
2301
2375
|
* out and the note in their place said only "N characters cut here" — while
|
|
2302
2376
|
* this tool's own note had promised each of those files that it "fits in one
|
|
2303
|
-
* call, so it is not paged":
|
|
2304
|
-
*
|
|
2305
|
-
* further calls (52,908 chars).
|
|
2377
|
+
* call, so it is not paged": one batched read of ten e2e specs (121,467 chars) lost the
|
|
2378
|
+
* middle six and bought them again in six further calls (52,908 chars).
|
|
2306
2379
|
*
|
|
2307
2380
|
* So the CALL gets the ceiling the request gives it. Whole files are returned
|
|
2308
2381
|
* up to it; the paths that did not fit are named in `note` and listed in
|
|
@@ -2337,7 +2410,7 @@ export const MAX_READ_RETURN_CHARS = MAX_READ_CALL_CHARS - READ_RESULT_ENVELOPE_
|
|
|
2337
2410
|
/**
|
|
2338
2411
|
* A file this small is returned whole on a second window of it (paging it costs a call each).
|
|
2339
2412
|
* The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
|
|
2340
|
-
* file under it "fits in ONE call", and a smaller bound here
|
|
2413
|
+
* file under it "fits in ONE call", and a smaller bound here breaks that promise.
|
|
2341
2414
|
*/
|
|
2342
2415
|
export const SMALL_FILE_WHOLE_CHARS = MAX_READ_RETURN_CHARS;
|
|
2343
2416
|
/**
|
|
@@ -2348,11 +2421,11 @@ export const SMALL_FILE_WHOLE_CHARS = MAX_READ_RETURN_CHARS;
|
|
|
2348
2421
|
* An entry document is boilerplate the reader rarely needs in full: meta tags,
|
|
2349
2422
|
* stylesheet and preload links, a `<style>` block, and a handful of `<script>`
|
|
2350
2423
|
* tags. The parts an executor acts on — the body's structure, where a script is
|
|
2351
|
-
* loaded — are all visible in an outline of one line per tag.
|
|
2352
|
-
* 63,735 chars
|
|
2424
|
+
* loaded — are all visible in an outline of one line per tag. One such read paid
|
|
2425
|
+
* 63,735 chars: ~60 KB of it was the IDE's own preview bridges,
|
|
2353
2426
|
* and the read pushed the transcript past its context cap, so the e2e specs the
|
|
2354
2427
|
* executor had just read were trimmed to summaries and it read the whole suite
|
|
2355
|
-
* again — twice
|
|
2428
|
+
* again — twice. The scaffold now keeps those
|
|
2356
2429
|
* bridges in separate files, but a template's own head is still ~5-12 KB, and a
|
|
2357
2430
|
* static-site or SSR entry can be far more.
|
|
2358
2431
|
*
|
|
@@ -2486,7 +2559,7 @@ export function truncateMiddle(s, maxLength) {
|
|
|
2486
2559
|
/**
|
|
2487
2560
|
* A test run's tally lines (` 3 failed`, ` 71 passed (4.6m)`) the cut text lost, put back at
|
|
2488
2561
|
* its end: Playwright lists every failed test after its "N failed" line, so a long list pushed
|
|
2489
|
-
* the line itself into the cut middle and the run
|
|
2562
|
+
* the line itself into the cut middle and the run reads as having no failures.
|
|
2490
2563
|
*
|
|
2491
2564
|
* @param original - The whole output.
|
|
2492
2565
|
* @param cut - The truncated output.
|
|
@@ -2601,9 +2674,8 @@ export const RUNNING_FAILURE_LINE_RE = '^[[:space:]]*(✘|✗|×|✖|✕|[0-9]+\
|
|
|
2601
2674
|
* the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
|
|
2602
2675
|
*
|
|
2603
2676
|
* A Playwright `list` run prints one line per test as it finishes, so the LAST bytes of a running
|
|
2604
|
-
* log are its least informative part —
|
|
2605
|
-
*
|
|
2606
|
-
* make anyway. A background run also carries each failure's own error as it fails
|
|
2677
|
+
* log are its least informative part — the last 3 000 bytes of a 139-test walk are the skip
|
|
2678
|
+
* list, and all 61 failures were above them, which costs the run the grep it has to make anyway. A background run also carries each failure's own error as it fails
|
|
2607
2679
|
* ({@link PLAYWRIGHT_FAILURES_INLINE_ENV}), and {@link RUNNING_FAILURE_LINE_RE} already matches
|
|
2608
2680
|
* those blocks' headers and their `Error:` lines, so this slice shows the causes too.
|
|
2609
2681
|
*
|
|
@@ -2642,11 +2714,11 @@ export const RUNNING_LOG_READ_BYTES = REPEATING_CAUSE_TAIL_BYTES;
|
|
|
2642
2714
|
* The shell command whose stdout is a bounded CONTIGUOUS read of a running task's own log: the
|
|
2643
2715
|
* log's byte size, then its last {@link RUNNING_LOG_READ_BYTES} bytes.
|
|
2644
2716
|
*
|
|
2645
|
-
* {@link runningOutputSliceCommand} is the wrong read for a diagnosis
|
|
2717
|
+
* {@link runningOutputSliceCommand} is the wrong read for a diagnosis: it
|
|
2646
2718
|
* stitches the first 40 grep-selected failure lines to the last 1 000 bytes with a blank line
|
|
2647
2719
|
* between, so the tail is DISJOINT from the headers above it. Read as one log, the last numbered
|
|
2648
|
-
* header absorbs whatever the tail starts with — a clipped skipped-test title
|
|
2649
|
-
* a genuine failure
|
|
2720
|
+
* header absorbs whatever the tail starts with — a clipped skipped-test title becomes the cause
|
|
2721
|
+
* of a genuine failure. A contiguous window can only ever hold real
|
|
2650
2722
|
* neighbouring lines of one log.
|
|
2651
2723
|
*
|
|
2652
2724
|
* The size rides along so a caller can say when the window is only the log's end
|
|
@@ -2778,8 +2850,8 @@ function failureBody(block) {
|
|
|
2778
2850
|
* pair of a failed assertion.
|
|
2779
2851
|
*
|
|
2780
2852
|
* Without this, every `TimeoutError: page.press: Timeout 5000ms exceeded.` in a log is one
|
|
2781
|
-
* message, because that line names the ACTION (`page.press`) and never its subject.
|
|
2782
|
-
*
|
|
2853
|
+
* message, because that line names the ACTION (`page.press`) and never its subject. Four such
|
|
2854
|
+
* failures in one run — one waiting on
|
|
2783
2855
|
* `button:has-text("Publish Article")`, one on `.article-preview h1`, one on
|
|
2784
2856
|
* `.article-preview').first().locator('.author')`, one on a generated `h1:has-text("Test
|
|
2785
2857
|
* Article …")` — and the run's summary called them two causes while its mid-run note called them
|
|
@@ -2815,8 +2887,8 @@ const FAILURE_EVIDENCE_CHARS = 200;
|
|
|
2815
2887
|
* `Expected: <n>`, and a signature built from a `waiting for` line never carries the assertion
|
|
2816
2888
|
* values at all. Those values are what a fix round needs, and the run's stdout they came from is
|
|
2817
2889
|
* cut in the MIDDLE by the tool-result cap, so a listed cause that does not carry them sends the
|
|
2818
|
-
* executor back to the log —
|
|
2819
|
-
*
|
|
2890
|
+
* executor back to the log — four `grep -A` round trips (~25 s) for the bodies of causes the
|
|
2891
|
+
* note had already listed.
|
|
2820
2892
|
*
|
|
2821
2893
|
* @param body - The block's lines, from {@link failureBody}.
|
|
2822
2894
|
* @param message - The cause's signature: a line it already carries is not repeated.
|
|
@@ -2860,7 +2932,7 @@ function failureEvidence(body, message) {
|
|
|
2860
2932
|
* The duration is the one that matters. A suite that sets `actionTimeout: 5_000` — the X1
|
|
2861
2933
|
* realworld suite does — spends `5 s` plus teardown on every "the locator never appeared"
|
|
2862
2934
|
* failure, so `~7 s` is a property of the CONFIG, not of the cause, and four different missing
|
|
2863
|
-
* elements print the same number
|
|
2935
|
+
* elements print the same number. Two tests waiting on the same
|
|
2864
2936
|
* locator for 5 s and for 10 s wait for the same thing; two waiting on different locators do not
|
|
2865
2937
|
* become one cause by timing out together.
|
|
2866
2938
|
*
|
|
@@ -2894,7 +2966,7 @@ function failureBudgetMs(block) {
|
|
|
2894
2966
|
* The cause signature with the budget it normalized away put back: `Test timeout of <n> exceeded`
|
|
2895
2967
|
* becomes `Test timeout of 15000ms exceeded`. Every other `<n>` stays — those are the values that
|
|
2896
2968
|
* differ between tests of one cause, while the budget is the one that does not, and it is what
|
|
2897
|
-
* turns "a timeout" into "15 creates in 15 s"
|
|
2969
|
+
* turns "a timeout" into "15 creates in 15 s".
|
|
2898
2970
|
*
|
|
2899
2971
|
* @param message - The normalized signature.
|
|
2900
2972
|
* @param budgetMs - The budget the cause's first block printed.
|
|
@@ -2956,9 +3028,8 @@ function testKey(t) {
|
|
|
2956
3028
|
* A retried failure prints twice in a `list` log — its first attempt, then the same test with
|
|
2957
3029
|
* `(retry #1)` after the title — while its failure block names it without the marker. Keyed
|
|
2958
3030
|
* literally, the retry row is a test no cause claims, so it fell through to the duration arm and
|
|
2959
|
-
* was listed as a "shared duration" cause beside the proven cause it repeated:
|
|
2960
|
-
*
|
|
2961
|
-
* duplicating the causes above them.
|
|
3031
|
+
* was listed as a "shared duration" cause beside the proven cause it repeated: every retry row
|
|
3032
|
+
* of a run became one of six extra causes duplicating the causes above them.
|
|
2962
3033
|
*
|
|
2963
3034
|
* @param t - A failed test, as a result line or a failure block spelled it.
|
|
2964
3035
|
* @returns The key a test's retry row shares with its failure block.
|
|
@@ -2998,13 +3069,13 @@ function distinctTests(tests) {
|
|
|
2998
3069
|
*
|
|
2999
3070
|
* Either way the point is the same: a suite whose failures repeat one cause spends its whole
|
|
3000
3071
|
* runtime proving that one cause, and the executor watching it can be told so at the third
|
|
3001
|
-
* failure instead of at the last.
|
|
3072
|
+
* failure instead of at the last. Fifteen tests walked in 4.0 minutes where 15.6 s
|
|
3002
3073
|
* each was the test timeout and one missing `navigate('/')` was the cause of all of them.
|
|
3003
3074
|
*
|
|
3004
3075
|
* The quorum is of DISTINCT tests — {@link REPEATING_FAILURE_MIN} of them — because a log that
|
|
3005
|
-
* prints each failure twice (a second reporter, a retry)
|
|
3006
|
-
* tests
|
|
3007
|
-
* with one cause in hand
|
|
3076
|
+
* prints each failure twice (a second reporter, a retry) has told a run that four of its
|
|
3077
|
+
* tests failed the same way when two had, so the executor stopped a 139-test walk at test 21
|
|
3078
|
+
* with one cause in hand.
|
|
3008
3079
|
*
|
|
3009
3080
|
* @param log - The log so far.
|
|
3010
3081
|
* @returns The repeating cause, or null when no signature has {@link REPEATING_FAILURE_MIN}
|
|
@@ -3083,20 +3154,25 @@ export function repeatingFailure(log) {
|
|
|
3083
3154
|
* A body for a test the request EXCLUDES is not that evidence: its failure does not count for
|
|
3084
3155
|
* this request ({@link matchingExcludedTitles}), so a wait that handed it over as the diagnostic
|
|
3085
3156
|
* spent a model round trip and 8 k of partial log on a test the same result says to ignore —
|
|
3086
|
-
*
|
|
3087
|
-
*
|
|
3157
|
+
* which a run has done twice while the platform's proof was still walking.
|
|
3158
|
+
*
|
|
3159
|
+
* With `finalOnly`, only a failure the run has FINISHED with counts; a pending retry waits.
|
|
3088
3160
|
*
|
|
3089
3161
|
* @param log - A log, or the slice of one a wait reads.
|
|
3090
3162
|
* @param excluded - The test titles the request carves out, if the caller knows them.
|
|
3163
|
+
* @param options - `finalOnly` reads only a failure the run has finished with; `noRetries` says
|
|
3164
|
+
* the command proves the run cannot retry (`--retries=0`), so its first failure is final.
|
|
3091
3165
|
* @returns True when a `✘` row's own test, outside the request's exclusions, has a failure body
|
|
3092
|
-
* under it.
|
|
3166
|
+
* under it — and, with `finalOnly`, that failure is final in the log read so far.
|
|
3093
3167
|
*/
|
|
3094
|
-
export function logShowsInlineFailureBodies(log, excluded = []) {
|
|
3168
|
+
export function logShowsInlineFailureBodies(log, excluded = [], options = {}) {
|
|
3095
3169
|
const text = log.replace(ANSI_SGR, '');
|
|
3096
3170
|
const failed = new Set(distinctTests(failedTests(text)).map(retrylessTestKey));
|
|
3097
3171
|
if (failed.size === 0)
|
|
3098
3172
|
return false;
|
|
3099
|
-
|
|
3173
|
+
const blocks = failureBlocks(text);
|
|
3174
|
+
const rows = options.finalOnly ? resultRows(text) : [];
|
|
3175
|
+
for (const block of blocks) {
|
|
3100
3176
|
const m = FAILURE_BLOCK_HEADER.exec(block.split('\n')[0] ?? '');
|
|
3101
3177
|
if (m === null)
|
|
3102
3178
|
continue;
|
|
@@ -3104,11 +3180,91 @@ export function logShowsInlineFailureBodies(log, excluded = []) {
|
|
|
3104
3180
|
const heading = where === '' ? title : `${where} › ${title}`;
|
|
3105
3181
|
if (matchingExcludedTitles(heading, excluded).length > 0)
|
|
3106
3182
|
continue;
|
|
3107
|
-
|
|
3183
|
+
const key = retrylessTestKey({ where, title, seconds: null });
|
|
3184
|
+
if (!failed.has(key))
|
|
3185
|
+
continue;
|
|
3186
|
+
if (!options.finalOnly)
|
|
3187
|
+
return true;
|
|
3188
|
+
if (failureIsFinal({ rows, blocks, key, noRetries: options.noRetries === true }))
|
|
3108
3189
|
return true;
|
|
3109
3190
|
}
|
|
3110
3191
|
return false;
|
|
3111
3192
|
}
|
|
3193
|
+
/** The runner's retry marker on a `list`-reporter row (` (retry #1)`, `_retrySuffix`). */
|
|
3194
|
+
const RETRY_ROW_SUFFIX = /\s*\(retry #\d+\)$/;
|
|
3195
|
+
/** The separator a retried failure's body opens its own attempt with (`formatFailure`). */
|
|
3196
|
+
const RETRY_BODY_SEPARATOR = /^\s*Retry #\d+\b/m;
|
|
3197
|
+
/**
|
|
3198
|
+
* Every result row a log carries, in the order the runner printed them.
|
|
3199
|
+
*
|
|
3200
|
+
* @param log - The log (ANSI already stripped).
|
|
3201
|
+
* @returns The rows, each with where it sits, what it is called, and which attempt it is.
|
|
3202
|
+
*/
|
|
3203
|
+
function resultRows(log) {
|
|
3204
|
+
const rows = [];
|
|
3205
|
+
for (const line of log.split('\n')) {
|
|
3206
|
+
const m = LIST_RESULT_LINE.exec(line);
|
|
3207
|
+
if (!m)
|
|
3208
|
+
continue;
|
|
3209
|
+
const { where, title } = splitTitlePath(m[2]);
|
|
3210
|
+
rows.push({
|
|
3211
|
+
mark: m[1],
|
|
3212
|
+
where,
|
|
3213
|
+
title,
|
|
3214
|
+
seconds: durationSeconds(m[3], m[4]),
|
|
3215
|
+
retry: RETRY_ROW_SUFFIX.test(title),
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return rows;
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* A failure block's test identity, keyed the way a result row is.
|
|
3222
|
+
*
|
|
3223
|
+
* @param block - One block from {@link failureBlocks}.
|
|
3224
|
+
* @returns The test key, or null when the block's header names no test.
|
|
3225
|
+
*/
|
|
3226
|
+
function failureBlockKey(block) {
|
|
3227
|
+
const m = FAILURE_BLOCK_HEADER.exec(block.split('\n')[0] ?? '');
|
|
3228
|
+
if (m === null)
|
|
3229
|
+
return null;
|
|
3230
|
+
const { where, title } = splitTitlePath(m[1] ?? '');
|
|
3231
|
+
return retrylessTestKey({ where, title, seconds: null });
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Whether a test's failure is FINAL in the log read so far — one the run is done with, not a
|
|
3235
|
+
* first attempt whose retry is still to come.
|
|
3236
|
+
*
|
|
3237
|
+
* A `list` run retries a failed test immediately, so the test's own next row says which attempt
|
|
3238
|
+
* the log has printed last: the same test's ` (retry #N)` row (final when that retry failed; a
|
|
3239
|
+
* flake the run absorbed when it passed), or a different test's row (no retry ran, so the failure
|
|
3240
|
+
* was final). A `Retry #N` separator in the test's body is the runner's own record that a retry
|
|
3241
|
+
* attempt ran and failed. A first attempt followed by nothing is only the last row READ — its
|
|
3242
|
+
* retry may not have printed yet — so it waits; a command that pins `--retries=0` proves there is
|
|
3243
|
+
* no retry to wait for.
|
|
3244
|
+
*
|
|
3245
|
+
* A deployed proof's yield handed back a first attempt (no `Retry #` separator) that then
|
|
3246
|
+
* passed on its retry, and the edit it prompted cost a redeploy plus a whole walk. The fix is
|
|
3247
|
+
* not to stop yielding — the retry-failed shape paid off — but to yield only on this one.
|
|
3248
|
+
*
|
|
3249
|
+
* @param args - The log's rows and blocks, the failing test's key, and whether the command
|
|
3250
|
+
* proves the run cannot retry.
|
|
3251
|
+
* @returns True when that test's failure is final in the log read so far.
|
|
3252
|
+
*/
|
|
3253
|
+
function failureIsFinal(args) {
|
|
3254
|
+
if (args.noRetries)
|
|
3255
|
+
return true;
|
|
3256
|
+
// The runner's own record that an attempt after the first ran and failed.
|
|
3257
|
+
if (args.blocks.some((block) => failureBlockKey(block) === args.key && RETRY_BODY_SEPARATOR.test(block)))
|
|
3258
|
+
return true;
|
|
3259
|
+
const own = args.rows.filter((row) => retrylessTestKey(row) === args.key);
|
|
3260
|
+
const last = own.at(-1);
|
|
3261
|
+
if (last === undefined)
|
|
3262
|
+
return false;
|
|
3263
|
+
if (last.retry)
|
|
3264
|
+
return last.mark === '✘';
|
|
3265
|
+
const at = args.rows.indexOf(last);
|
|
3266
|
+
return args.rows.slice(at + 1).some((row) => retrylessTestKey(row) !== args.key);
|
|
3267
|
+
}
|
|
3112
3268
|
/** How many of a repeating cause's failures the note lists before it says "and N more". */
|
|
3113
3269
|
export const REPEATING_FAILURE_LISTED = 5;
|
|
3114
3270
|
/**
|
|
@@ -3119,8 +3275,8 @@ export const REPEATING_FAILURE_LISTED = 5;
|
|
|
3119
3275
|
*/
|
|
3120
3276
|
export const TOOL_RESULT_CHARS = 8000;
|
|
3121
3277
|
/**
|
|
3122
|
-
* The characters a finished run's cause list may use of {@link TOOL_RESULT_CHARS}.
|
|
3123
|
-
*
|
|
3278
|
+
* The characters a finished run's cause list may use of {@link TOOL_RESULT_CHARS}. A measured
|
|
3279
|
+
* recap was `12 failures, 8 causes`: one cause line is
|
|
3124
3280
|
* ~200 chars and ~80 more with its two body lines, so 8 causes is ~2.3 k against the 8 k cap —
|
|
3125
3281
|
* the old 5-cause cap withheld 3 causes that the same result had room for.
|
|
3126
3282
|
*
|
|
@@ -3144,8 +3300,8 @@ export const FAILURE_CAUSE_TESTS_LISTED = 6;
|
|
|
3144
3300
|
*
|
|
3145
3301
|
* Used only where a COUNT of files is what the sentence needs; every note that names a
|
|
3146
3302
|
* test to the executor prints the path the log wrote, because that is the path its own
|
|
3147
|
-
* re-run hint names and the one the executor can paste back into a command (
|
|
3148
|
-
*
|
|
3303
|
+
* re-run hint names and the one the executor can paste back into a command (a shortened
|
|
3304
|
+
* `e2e/auth.spec.ts:19` reads as a file the app owned, and rewrites its config
|
|
3149
3305
|
* over a suite path that was already right).
|
|
3150
3306
|
*
|
|
3151
3307
|
* @param file - A path as {@link specFile} returns it.
|
|
@@ -3211,7 +3367,7 @@ export const UNIVERSAL_CAUSE_FILES = 3;
|
|
|
3211
3367
|
* THREE, and the number is the cost of the alternative. The old floor of 15 was chosen when the
|
|
3212
3368
|
* reading was only offered at a completed run; once it STOPS the walk, every test waited for is
|
|
3213
3369
|
* a test whose result is thrown away, and a blank page costs its per-test budget each time
|
|
3214
|
-
* (
|
|
3370
|
+
* (18 tests, ~4.2 minutes, on a page whose thrown error was visible in the first
|
|
3215
3371
|
* `pageerror`). Three is the smallest count that is not one page's own bug: the cause additionally
|
|
3216
3372
|
* needs three DISTINCT spec files (`UNIVERSAL_CAUSE_FILES`, or the files a narrowed run selected),
|
|
3217
3373
|
* no failure showing a locator RESOLVING ({@link ELEMENT_FOUND} — a blank page cannot resolve
|
|
@@ -3236,8 +3392,8 @@ const LOCATOR_WAIT = /waiting for locator\(|element\(s\) not found|toBeVisible\(
|
|
|
3236
3392
|
* of the viewport`). A blank page cannot resolve a locator, so one of these anywhere in a run
|
|
3237
3393
|
* disproves {@link emptyPageCause}.
|
|
3238
3394
|
*
|
|
3239
|
-
*
|
|
3240
|
-
*
|
|
3395
|
+
* Two re-runs narrowed to the tests that had just failed were stopped as "the app is rendering
|
|
3396
|
+
* NOTHING" while every block in the log
|
|
3241
3397
|
* carried `locator resolved to <i class="ion-close-circled"></i>` followed by `element is not
|
|
3242
3398
|
* visible` — the page was drawn, and only an icon had no box.
|
|
3243
3399
|
*/
|
|
@@ -3284,10 +3440,10 @@ function pageErrorInLog(text) {
|
|
|
3284
3440
|
*
|
|
3285
3441
|
* This is ONE cause even though the signatures differ — each spec asks for its own first element
|
|
3286
3442
|
* (`input[name="username"]`, `a.navbar-brand`, `h1`), so {@link repeatingFailure} cannot group
|
|
3287
|
-
* them and {@link universalCause}'s signature test cannot see the run is over.
|
|
3288
|
-
*
|
|
3289
|
-
* first locator, and the note told the run to keep walking for another ~9 minutes.
|
|
3290
|
-
*
|
|
3443
|
+
* them and {@link universalCause}'s signature test cannot see the run is over. One run
|
|
3444
|
+
* walked a built app that rendered nothing: 36 tests had failed, every one at its
|
|
3445
|
+
* first locator, and the note told the run to keep walking for another ~9 minutes. Another
|
|
3446
|
+
* repeated it: 18 tests, one whole walk, on a page whose
|
|
3291
3447
|
* `PAGEERROR No 'auth-client' provider bonded` was in the log from the first one.
|
|
3292
3448
|
*
|
|
3293
3449
|
* @param text - The log (ANSI already stripped).
|
|
@@ -3391,8 +3547,8 @@ const PLAYWRIGHT_VALUE_FLAGS = new Set([
|
|
|
3391
3547
|
* whole suite (or its selection cannot be read).
|
|
3392
3548
|
*
|
|
3393
3549
|
* {@link universalCause}'s file threshold is written for a whole-suite run, which a narrowed run
|
|
3394
|
-
* can never reach.
|
|
3395
|
-
*
|
|
3550
|
+
* can never reach. A run that re-ran ONE spec file against a hand-started `vite preview` the
|
|
3551
|
+
* browser could not reach had every test in it fail `page.goto: Timeout` —
|
|
3396
3552
|
* but one file is below {@link UNIVERSAL_CAUSE_FILES}, so the wait sat out its entire 280 s
|
|
3397
3553
|
* budget on a run that had already measured all it could.
|
|
3398
3554
|
*
|
|
@@ -3441,7 +3597,7 @@ export function specFilesSelected(command) {
|
|
|
3441
3597
|
*
|
|
3442
3598
|
* A command that names `file:line` tests is a re-run of tests that already failed — the shape
|
|
3443
3599
|
* {@link emptyPageCause} must not read as "the page renders nothing", because the executor is
|
|
3444
|
-
* verifying fixes and the stop throws that verdict away (
|
|
3600
|
+
* verifying fixes and the stop throws that verdict away (twice).
|
|
3445
3601
|
*
|
|
3446
3602
|
* @param command - The run's command.
|
|
3447
3603
|
* @returns The selected test references, or null when the selection is not per-test.
|
|
@@ -3499,7 +3655,7 @@ function sameTestRef(ref, where) {
|
|
|
3499
3655
|
* threshold, and nothing has passed since its first failure. The threshold is
|
|
3500
3656
|
* {@link UNIVERSAL_CAUSE_FILES} for a whole-suite run but the number of files the command
|
|
3501
3657
|
* SELECTED for a narrowed one ({@link specFilesSelected}): a one-file run can never span three,
|
|
3502
|
-
* and
|
|
3658
|
+
* and a run can wait out its whole budget on exactly that shape. An app
|
|
3503
3659
|
* that is not up, a 5xx on every request, or a selector that is missing on every page all look
|
|
3504
3660
|
* like that, and every remaining test will fail the same way — so the run's remaining minutes
|
|
3505
3661
|
* measure nothing. A CONNECTION-class cause ({@link CONNECTION_CAUSE}) needs no file span at all:
|
|
@@ -3508,8 +3664,8 @@ function sameTestRef(ref, where) {
|
|
|
3508
3664
|
*
|
|
3509
3665
|
* Anything less is NOT this: a cause that stops at one file of a multi-file run, or that leaves
|
|
3510
3666
|
* other tests passing, or that shares the run with failures of another shape, leaves the run with
|
|
3511
|
-
* something to say, and the run is the only thing that will say it.
|
|
3512
|
-
*
|
|
3667
|
+
* something to say, and the run is the only thing that will say it. Stopping runs at their first
|
|
3668
|
+
* repeating cause has burned eight local runs over 14.5 minutes without finishing
|
|
3513
3669
|
* one; three of its deploys each fixed a single cause that a finished run would have listed
|
|
3514
3670
|
* alongside the others.
|
|
3515
3671
|
*
|
|
@@ -3565,14 +3721,14 @@ export function universalCause(log, found, command) {
|
|
|
3565
3721
|
* spec-file threshold — {@link UNIVERSAL_CAUSE_FILES}, or the files a narrowed run's own
|
|
3566
3722
|
* command selects, or {@link CONNECTION_CAUSE_TESTS} tests of an app that never answered — with
|
|
3567
3723
|
* nothing passing since the first. That is the
|
|
3568
|
-
* dont-stop-runs rule.
|
|
3569
|
-
*
|
|
3724
|
+
* dont-stop-runs rule. Stopping runs at their first repeating cause has burned eight local
|
|
3725
|
+
* runs in 14.5 minutes without finishing one, and a run that still has other causes
|
|
3570
3726
|
* left to name is the only thing that will name them.
|
|
3571
3727
|
* - **A PROVEN cause** is an error repeated VERBATIM. A shared DURATION is a LEAD, never a cause:
|
|
3572
3728
|
* a suite whose config sets a timeout spends that timeout on every failure that waits for
|
|
3573
3729
|
* anything, so three unrelated tests timing out together look exactly like one broken run
|
|
3574
|
-
* (`~7 s` is a property of the CONFIG, not of the cause — {@link normalizedSignature}).
|
|
3575
|
-
*
|
|
3730
|
+
* (`~7 s` is a property of the CONFIG, not of the cause — {@link normalizedSignature}). A run
|
|
3731
|
+
* told `status: 'repeating_failure'` twice on that
|
|
3576
3732
|
* shape — three `url-navigation.spec.ts` tests at ~16.2 s, one `timeout: 15_000` plus teardown
|
|
3577
3733
|
* — read the early return as "stop waiting, edit now", and voided a run that was still walking;
|
|
3578
3734
|
* the second cost a 224 s wait that ended with no result at all.
|
|
@@ -3606,12 +3762,11 @@ export function earlyReturnCause(found, universal) {
|
|
|
3606
3762
|
* extracted by molecule-dev's `excludedTestTitles`) while a run's heading carries more of it
|
|
3607
3763
|
* (`comments.spec.ts › Comments › should require login to post comment`), so containment is the
|
|
3608
3764
|
* comparison. Whitespace is normalised and case is ignored: a brief may wrap the title across a
|
|
3609
|
-
* line
|
|
3765
|
+
* line, and the reporter's own casing never has to match the brief's.
|
|
3610
3766
|
*
|
|
3611
3767
|
* Two readers used to answer this separately — the causes note (which therefore asked the
|
|
3612
3768
|
* executor to fix a test the same result said not to) and molecule-dev's exclusion notes — and
|
|
3613
|
-
*
|
|
3614
|
-
* test).
|
|
3769
|
+
* a run paid for the gap (a turn and an edit spent on the excluded test).
|
|
3615
3770
|
*
|
|
3616
3771
|
* @param heading - The text to match against: a test's title, its whole `file › describe › title`
|
|
3617
3772
|
* path, or (for a note that only has the run's output) the run's text.
|
|
@@ -3634,7 +3789,7 @@ export function matchingExcludedTitles(heading, excluded) {
|
|
|
3634
3789
|
*
|
|
3635
3790
|
* The point is that ONE run yields EVERY cause. A Playwright report prints its failures one
|
|
3636
3791
|
* after another with no statement of which of them share a cause, so a run of 19 failures is 19
|
|
3637
|
-
* bodies to read;
|
|
3792
|
+
* bodies to read; three deploys (6.7 minutes) each fixed a
|
|
3638
3793
|
* single cause, two of them found by runs stopped before they walked the rest.
|
|
3639
3794
|
*
|
|
3640
3795
|
* The signature is the cause itself where the log carries the failures' bodies (a finished run, a
|
|
@@ -3646,8 +3801,8 @@ export function matchingExcludedTitles(heading, excluded) {
|
|
|
3646
3801
|
* A test the request EXCLUDES by name is not a cause to fix: its failure does not count for
|
|
3647
3802
|
* this request ({@link matchingExcludedTitles}), so it is dropped here rather than handed to the
|
|
3648
3803
|
* executor as work — a result that names it beside "it does not count" is the contradiction
|
|
3649
|
-
*
|
|
3650
|
-
*
|
|
3804
|
+
* a run filed (the model spent a turn and an edit on the excluded test, on a page that was
|
|
3805
|
+
* passing).
|
|
3651
3806
|
*
|
|
3652
3807
|
* @param log - The log of a finished or running run.
|
|
3653
3808
|
* @param excluded - The test titles the request carves out, if the caller knows them.
|
|
@@ -3667,8 +3822,8 @@ const FLAKY_TALLY_LINE = /^\s*[1-9]\d*\s+flaky\b/;
|
|
|
3667
3822
|
*
|
|
3668
3823
|
* Playwright prints a numbered failure block for the FIRST attempt of a test that then passes on
|
|
3669
3824
|
* its retry, and also declares the test under `N flaky` — so a reader that only looks at the
|
|
3670
|
-
* blocks counts a flake as a cause to fix.
|
|
3671
|
-
*
|
|
3825
|
+
* blocks counts a flake as a cause to fix. A proof run against the deployed URL reported ten
|
|
3826
|
+
* `waitForURL` timing flakes as "causes" of a red run, and the executor
|
|
3672
3827
|
* spent its next minutes re-running them one by one until three passed untouched. The
|
|
3673
3828
|
* declaration under the count is the runner's own answer, and its heading carries the same words
|
|
3674
3829
|
* as the failure block's ({@link splitTitlePath} reads both the same way).
|
|
@@ -3825,7 +3980,7 @@ function groupFailures(log, excluded, pageErrors = []) {
|
|
|
3825
3980
|
/**
|
|
3826
3981
|
* The error every test of a Playwright run reports when the run loaded no base URL: `page.goto('/')`
|
|
3827
3982
|
* has nothing to resolve against, so the browser refuses the navigation before any page loads.
|
|
3828
|
-
*
|
|
3983
|
+
* One run got ten of these and a list that called them ten causes.
|
|
3829
3984
|
*/
|
|
3830
3985
|
const NO_BASE_URL_ERROR = /Cannot navigate to invalid URL/i;
|
|
3831
3986
|
/** The browser's own `net::ERR_*`, as Playwright prints it for a document that never loaded. */
|
|
@@ -3850,7 +4005,7 @@ function everyCauseIsNavigationTimeout(causes) {
|
|
|
3850
4005
|
* command loaded no base URL. The line names the config the run should have loaded (the caller
|
|
3851
4006
|
* passes the one the platform recorded for this suite) and hands over the exact narrowed command
|
|
3852
4007
|
* with it, so the next run measures the app instead of spending another six minutes on the same
|
|
3853
|
-
* configuration mistake (
|
|
4008
|
+
* configuration mistake (one run sent four such runs in six minutes).
|
|
3854
4009
|
*
|
|
3855
4010
|
* @param causes - The run's failures, all carrying the same error.
|
|
3856
4011
|
* @param run - How the run was started, when the caller knows it.
|
|
@@ -3986,7 +4141,7 @@ const LANDMARK_WORD = {
|
|
|
3986
4141
|
* The sentence for a run whose navigational elements are all missing from pages that otherwise
|
|
3987
4142
|
* painted — the missing shared header the executor must check BEFORE rewriting auth.
|
|
3988
4143
|
*
|
|
3989
|
-
*
|
|
4144
|
+
* A run had four failures on `/` — `a[href="/profile/…"]`,
|
|
3990
4145
|
* `a.navbar-brand`, `nav a[href="/"]`, `nav .user-pic` — while every `error-context.md` snapshot
|
|
3991
4146
|
* showed the feed, sidebar and footer with no navigation at all. `Home.tsx` imported and
|
|
3992
4147
|
* re-exported `Navbar` but never rendered it, and `App.tsx` mounted `<Home />` in a bare route.
|
|
@@ -4080,7 +4235,7 @@ export function failureCausesNote(log, excluded = [], run, pageErrors = [],
|
|
|
4080
4235
|
* Spec files whose selector-contract rows an edit broke since the last whole walk, when the
|
|
4081
4236
|
* caller knows them: the narrowed re-run names them beside the failing tests. A green re-run of
|
|
4082
4237
|
* the failure alone would certify a tree it never measured, and the specs selecting the
|
|
4083
|
-
* element the edit removed were never walked (
|
|
4238
|
+
* element the edit removed were never walked (a real tree shipped that).
|
|
4084
4239
|
*/
|
|
4085
4240
|
regressedSpecs = []) {
|
|
4086
4241
|
const { causes, ignored, flaky } = groupFailures(log, excluded, pageErrors);
|
|
@@ -4196,9 +4351,9 @@ regressedSpecs = []) {
|
|
|
4196
4351
|
* failure's two most useful body lines underneath.
|
|
4197
4352
|
*
|
|
4198
4353
|
* The path is the one the log itself wrote, never a shortened one: it is relative to the
|
|
4199
|
-
* command's own cwd, so it is the form the executor can paste back (
|
|
4200
|
-
* `e2e/auth.spec.ts:19` as the suite's own stale starter spec,
|
|
4201
|
-
* it
|
|
4354
|
+
* command's own cwd, so it is the form the executor can paste back (a shortened
|
|
4355
|
+
* `e2e/auth.spec.ts:19` read as the suite's own stale starter spec, and a Playwright config
|
|
4356
|
+
* rewritten over it deleted the app's tests — 4 minutes of a 60-minute run on a working config).
|
|
4202
4357
|
*
|
|
4203
4358
|
* @param cause - The cause to write.
|
|
4204
4359
|
* @param index - Its position in the recap (0-based; printed 1-based).
|
|
@@ -4236,8 +4391,8 @@ function failureCauseLine(cause, index) {
|
|
|
4236
4391
|
* found at, and the ONE command that prints their bodies.
|
|
4237
4392
|
*
|
|
4238
4393
|
* Without it the old note said "… and 3 more causes" and left the executor to find them in a
|
|
4239
|
-
* stdout whose middle the tool-result cut had already removed —
|
|
4240
|
-
*
|
|
4394
|
+
* stdout whose middle the tool-result cut had already removed — four `grep -A` round trips
|
|
4395
|
+
* (~25 s) naming three causes the platform had withheld, and a
|
|
4241
4396
|
* weaker turn reads the listed ones and skips the rest.
|
|
4242
4397
|
*
|
|
4243
4398
|
* @param omitted - The causes past the recap's budget.
|
|
@@ -4298,7 +4453,7 @@ function omittedCausesRecap(omitted, run, excluded) {
|
|
|
4298
4453
|
* @param run - The run in flight: the command it was started with, as it was run, the directory
|
|
4299
4454
|
* that command ran in, the config it loaded when the caller knows it, and the settings the
|
|
4300
4455
|
* request required and the run left out (`env`) — the re-run carries them, so it never
|
|
4301
|
-
* reproduces the configuration the run was measured under by mistake
|
|
4456
|
+
* reproduces the configuration the run was measured under by mistake.
|
|
4302
4457
|
* @param locations - The failing `file:line`s to re-run, as the run's log printed them.
|
|
4303
4458
|
* @param excluded - The test titles the request carves out: the re-run inverts them out
|
|
4304
4459
|
* (`--grep-invert`) so it cannot report them again.
|
|
@@ -4332,18 +4487,18 @@ export function rerunOfRunningSuite(run, locations, excluded = []) {
|
|
|
4332
4487
|
* The note names the cause early so the executor can START FIXING while the run keeps walking,
|
|
4333
4488
|
* and it hands a stop command over only when {@link earlyReturnCause} says so — the SAME
|
|
4334
4489
|
* predicate the wait uses to decide whether to return early, so a finding the wait keeps waiting
|
|
4335
|
-
* on is a finding this note never asks to stop for.
|
|
4490
|
+
* on is a finding this note never asks to stop for. One run spent 4.0 minutes
|
|
4336
4491
|
* walking 15 tests whose one cause was certain at the third, 47 s in, and the note answers
|
|
4337
|
-
* exactly that;
|
|
4492
|
+
* exactly that; another stopped eight runs at their first repeating cause in 14.5
|
|
4338
4493
|
* minutes and never finished one, so the note must not say to stop when the run still has other
|
|
4339
|
-
* causes to name.
|
|
4494
|
+
* causes to name. The third shape: a shared duration is
|
|
4340
4495
|
* not a cause at all, so a lead reaches the executor as a note on a result it is still waiting
|
|
4341
4496
|
* for — never as a verdict with a stop command in it.
|
|
4342
4497
|
*
|
|
4343
4498
|
* The run is STILL WALKING while this note is written — both callers produce it mid-run — so a
|
|
4344
4499
|
* re-run of one of its tests is a second run against the same app and database, and the platform
|
|
4345
4500
|
* refuses that ({@link suiteRunInTheWay}, the same rule its overlap hold answers with):
|
|
4346
|
-
*
|
|
4501
|
+
* an executor read *"Start on them now, while the run keeps walking.
|
|
4347
4502
|
* `null-fields.spec.ts:53` alone reproduces the first one."* and sent exactly that re-run 23 s
|
|
4348
4503
|
* later, where the hold answered *"A playwright suite is already running against this app and
|
|
4349
4504
|
* database … Wait for it."* The note therefore hands over the read-and-fix to do NOW and the
|
|
@@ -4537,7 +4692,7 @@ export const PARSE_CHECKABLE = /\.(m?js|cjs|jsx|ts|tsx|mts|cts|json)$/;
|
|
|
4537
4692
|
* Basenames TypeScript reads as JSONC: `tsconfig.json`, `tsconfig.*.json` and
|
|
4538
4693
|
* the `jsconfig` counterparts. Comments and trailing commas are legal there —
|
|
4539
4694
|
* `tsc` accepts them — so a strict `JSON.parse` reports a valid project config
|
|
4540
|
-
* as broken (
|
|
4695
|
+
* as broken (a commented `tsconfig.conduit-suite.json` was
|
|
4541
4696
|
* answered with "every build and test fails until it does").
|
|
4542
4697
|
*/
|
|
4543
4698
|
const TYPESCRIPT_CONFIG = /^(?:ts|js)config(?:\.[^/\\]+)?\.json$/;
|