@molecule/api-ai-tools 1.13.1 → 1.14.1
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 +135 -42
- package/dist/command-position.d.ts +26 -0
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +314 -59
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/suite-overlap.d.ts +3 -1
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +130 -7
- package/dist/tools.d.ts +15 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +91 -20
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities.d.ts +49 -28
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +317 -70
- package/package.json +1 -1
package/dist/utilities.js
CHANGED
|
@@ -536,9 +536,25 @@ const PRINTING_REPORTERS = new Set(['list', 'line', 'dot', 'basic', 'spec']);
|
|
|
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';
|
|
539
|
+
/**
|
|
540
|
+
* A step that stores the status of the step before it in a variable: `__proof_slow=$?`.
|
|
541
|
+
*
|
|
542
|
+
* The variable name is the shell's own (`[A-Za-z_][A-Za-z0-9_]*`) and the value is EXACTLY the
|
|
543
|
+
* special parameter, bare or double-quoted, so nothing else can be running here. A capture whose
|
|
544
|
+
* value is anything else — `x=$(rebuild)`, `x=$?; rm -rf …` (a separate statement anyway) — is a
|
|
545
|
+
* program, not a status, and is not one of these.
|
|
546
|
+
*/
|
|
547
|
+
const STATUS_CAPTURE_STEP = /^[A-Za-z_][A-Za-z0-9_]*=(?:\$\?|"\$\?"|\$\{\?\})$/;
|
|
548
|
+
/**
|
|
549
|
+
* The arithmetic body `exit $((…))` may carry and still run nothing: variable names, numbers and
|
|
550
|
+
* arithmetic operators only. No `$`, backtick, `;` or comma is allowed, so no command substitution
|
|
551
|
+
* and no expansion of a variable into something that runs can hide in it.
|
|
552
|
+
*/
|
|
553
|
+
const STATUS_EXIT_ARITHMETIC = /^[A-Za-z0-9_|&+\-*/%^<>=!~() \t]*$/;
|
|
539
554
|
/**
|
|
540
555
|
* Whether a command step only reports or discards the status of the steps before it: `echo EXIT
|
|
541
|
-
* $?`, `echo $?`, `printf …`, `true`, `exit 0
|
|
556
|
+
* $?`, `echo $?`, `printf …`, `true`, `exit 0`, a status capture (`__proof_slow=$?`), or an
|
|
557
|
+
* `exit $((a | b))` over statuses already captured.
|
|
542
558
|
*
|
|
543
559
|
* The shape models and the platform's own re-run notes send routinely ends on one of these —
|
|
544
560
|
* `npx playwright test … > /tmp/pw.log 2>&1; echo EXIT $?` — and reading the split's last part as
|
|
@@ -548,12 +564,33 @@ export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_IN
|
|
|
548
564
|
* punctuation are not part of the step; {@link streamingBackgroundCommand} strips those before
|
|
549
565
|
* asking, and strips them here too for the split it is given.
|
|
550
566
|
*
|
|
567
|
+
* A status CAPTURE is the same kind of step, and the platform's own chained proof walks end on a
|
|
568
|
+
* run of them: `… <browser run>; __proof_slow=$?; echo __PROOF_EXIT__ …; exit $((__proof_fast |
|
|
569
|
+
* __proof_slow))` (`chainedProofCommand`, molecule-dev `post-deploy-proof-run.ts`). Reading the
|
|
570
|
+
* browser half as a status step is what lets the run keep its reporter, its machine report and the
|
|
571
|
+
* inline-failure switch — without it, every platform proof ran its browser suite on the request's
|
|
572
|
+
* own `line` reporter, which writes no ✓/✘ mark and no per-failure error into the log the wait
|
|
573
|
+
* reads (xr-molecule-flash-x03 +27:44).
|
|
574
|
+
*
|
|
575
|
+
* Only the two shapes whose whole value is the shell's own status are read this way; anything that
|
|
576
|
+
* could run — a command substitution, a backticked word, a suffix after the status — stays a step.
|
|
577
|
+
*
|
|
551
578
|
* @param step - One statement of the command, as the `&&`/`||`/`;` split left it.
|
|
552
579
|
* @returns True when the step runs nothing but a status report.
|
|
553
580
|
*/
|
|
554
581
|
function isStatusOnlyStep(step) {
|
|
555
|
-
const text = step.
|
|
556
|
-
|
|
582
|
+
const text = step.trim();
|
|
583
|
+
// An enclosing group's closer is punctuation, not part of the step — but the `))` that closes an
|
|
584
|
+
// arithmetic expansion is part of the statement, so that shape is read off the text as written.
|
|
585
|
+
const withoutCloser = text.replace(/[\s)}]+$/, '');
|
|
586
|
+
if (/^(?:echo|printf)\b/.test(withoutCloser) || /^true$/.test(withoutCloser))
|
|
587
|
+
return true;
|
|
588
|
+
if (/^exit(?:\s+\S+)?$/.test(withoutCloser))
|
|
589
|
+
return true;
|
|
590
|
+
if (STATUS_CAPTURE_STEP.test(withoutCloser))
|
|
591
|
+
return true;
|
|
592
|
+
const arithmetic = /^exit\s+\$\(\((.*)\)\)$/.exec(text);
|
|
593
|
+
return arithmetic !== null && STATUS_EXIT_ARITHMETIC.test(arithmetic[1] ?? '');
|
|
557
594
|
}
|
|
558
595
|
/**
|
|
559
596
|
* Rewrite a command about to run in the background so its log grows while it
|
|
@@ -565,13 +602,15 @@ function isStatusOnlyStep(step) {
|
|
|
565
602
|
* It is dropped: `wait_for_task` already returns the end of the log.
|
|
566
603
|
* - any output-only filter piped after a SUITE runner (`| grep … | tail -20`), which hides the
|
|
567
604
|
* failing test's own error and leaves the pipeline's exit status — the filter's — as the run's.
|
|
568
|
-
* Every such stage is dropped
|
|
569
|
-
* returns the end of the full log,
|
|
570
|
-
* (xr-molecule-flash-m101 +25:54).
|
|
605
|
+
* Every such stage is dropped from EVERY suite run in the command, not only the last
|
|
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 (xr-molecule-flash-m101 +25:54).
|
|
571
608
|
* - `playwright test` with no `--reporter`, which runs the config's reporter —
|
|
572
609
|
* often `html`, which prints nothing per test. `--reporter=list` is added
|
|
573
610
|
* when the playwright run is the command's last step, piped or not: it writes each result as
|
|
574
|
-
* it finishes, and it is the shape the platform reads a background suite back in.
|
|
611
|
+
* it finishes, and it is the shape the platform reads a background suite back in. Every EARLIER
|
|
612
|
+
* Playwright invocation gets the same reporter, so its log is readable while it walks; the
|
|
613
|
+
* machine `json` report stays on the last invocation.
|
|
575
614
|
*
|
|
576
615
|
* The run ends up with exactly ONE reporter that prints each failure — `list`. A run that names
|
|
577
616
|
* any other printing reporter has it REPLACED, never joined: two of them write every test result
|
|
@@ -634,20 +673,22 @@ export function streamingBackgroundCommand(command, context, reportPath) {
|
|
|
634
673
|
// A filter after the suite runner is not output the platform can read: it hides the failing
|
|
635
674
|
// test's own error, and its exit status replaces the run's with the filter's — m101's
|
|
636
675
|
// `… | grep -E "✘|passed|failed|skipped" | tail -20` reported `exitCode: 0` for a run with a
|
|
637
|
-
// failure in it. Every stage the parse proves is output-only is dropped
|
|
638
|
-
// log comes back anyway.
|
|
639
|
-
const filtered =
|
|
640
|
-
if (filtered.
|
|
676
|
+
// failure in it. Every stage the parse proves is output-only is dropped from EVERY run, earlier
|
|
677
|
+
// walks included; the end of the FULL log comes back anyway.
|
|
678
|
+
const filtered = suiteFilterDrops(out);
|
|
679
|
+
if (filtered.drops.length > 0) {
|
|
641
680
|
out = filtered.command;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
681
|
+
for (const dropped of filtered.drops) {
|
|
682
|
+
// A lone trailing `| tail -N` keeps its own sentence: the reason it goes is that it holds all
|
|
683
|
+
// output back until the command ends, not that a filter cannot be read back.
|
|
684
|
+
const tailOnly = TRAILING_TAIL.exec(dropped);
|
|
685
|
+
changes.push(tailOnly && tailOnly[0].trim() === dropped
|
|
686
|
+
? `Dropped the trailing \`${dropped}\`: it holds all output back until the command ends, ` +
|
|
687
|
+
'and wait_for_task returns the end of the log anyway.'
|
|
688
|
+
: `Dropped \`${dropped}\` from after the test runner: a filtered log cannot be ` +
|
|
689
|
+
'diagnosed — the failing test’s own error is not in it — and wait_for_task returns the ' +
|
|
690
|
+
'end of the full log anyway.');
|
|
691
|
+
}
|
|
651
692
|
}
|
|
652
693
|
const tail = TRAILING_TAIL.exec(out);
|
|
653
694
|
if (tail) {
|
|
@@ -863,8 +904,116 @@ export function streamingBackgroundCommand(command, context, reportPath) {
|
|
|
863
904
|
if (addToRun('--retries=0'))
|
|
864
905
|
changes.push(`Added \`--retries=0\`: ${why}.`);
|
|
865
906
|
}
|
|
907
|
+
// Every EARLIER Playwright invocation writes the same log, so it is put on the progressive
|
|
908
|
+
// reporter too — not only the invocation this launcher measures (see
|
|
909
|
+
// {@link earlierPlaywrightReporters}).
|
|
910
|
+
const earlier = earlierPlaywrightReporters(out);
|
|
911
|
+
if (earlier.changes.length > 0) {
|
|
912
|
+
out = earlier.command;
|
|
913
|
+
changes.push(...earlier.changes);
|
|
914
|
+
}
|
|
866
915
|
return { command: out, changes, ...(report === null ? {} : { reportPath: report }) };
|
|
867
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* Put every Playwright invocation BEFORE the command's last one on the progressive `list`
|
|
919
|
+
* reporter.
|
|
920
|
+
*
|
|
921
|
+
* A compound command's earlier walk is not a preview: it writes the same log the executor reads,
|
|
922
|
+
* and on X1 xr-molecule-flash-l21 (+21:27) the first 3.6-minute walk kept its own
|
|
923
|
+
* `--reporter=line`, which prints `[n/N] <title>` rows with no ✓/✘ mark and no per-failure error.
|
|
924
|
+
* What came back was `72 passed` and two failure NAMES; the error bodies existed only in the walk's
|
|
925
|
+
* discarded log, and diagnosing them needed another run.
|
|
926
|
+
*
|
|
927
|
+
* Only the reporter SET is touched, and only printing reporters in it ({@link PRINTING_REPORTERS})
|
|
928
|
+
* are replaced by ONE `list` — the same rule the last invocation gets. A run that names no
|
|
929
|
+
* reporter has the config reporter, which may print nothing until the end, so `--reporter=list` is
|
|
930
|
+
* added where the parse can place it. Every other flag (`--grep`, `--project`, `--config`,
|
|
931
|
+
* `--workers`, `--timeout`, selections, exclusions, retries) and every non-printing reporter
|
|
932
|
+
* (`json`, `html`, `junit`, or one this build does not know) is left exactly as written.
|
|
933
|
+
*
|
|
934
|
+
* The LAST invocation is deliberately not this function's: {@link streamingBackgroundCommand}
|
|
935
|
+
* already decides its reporters, and it alone owns the machine `json` report, which must never be
|
|
936
|
+
* claimed for an earlier walk. A statement the parse cannot place (inside a nested `sh -c` string)
|
|
937
|
+
* is left as written — there is no offset in this command to splice at.
|
|
938
|
+
*
|
|
939
|
+
* @param command - The command after every other rewrite.
|
|
940
|
+
* @returns The rewritten command, and one sentence per reporter change made.
|
|
941
|
+
*/
|
|
942
|
+
function earlierPlaywrightReporters(command) {
|
|
943
|
+
const located = locatedSegments(command);
|
|
944
|
+
const runs = [];
|
|
945
|
+
for (let i = 0; i < located.length; i += 1) {
|
|
946
|
+
const segment = located[i]?.segment;
|
|
947
|
+
if (segment && SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)))
|
|
948
|
+
runs.push(i);
|
|
949
|
+
}
|
|
950
|
+
const rewrites = [];
|
|
951
|
+
let out = command;
|
|
952
|
+
// Right to left, so each site's offsets still describe the text they were measured on.
|
|
953
|
+
for (const at of runs.slice(0, -1).reverse()) {
|
|
954
|
+
const site = located[at];
|
|
955
|
+
if (!site || site.start === null || site.end === null)
|
|
956
|
+
continue;
|
|
957
|
+
const raw = out.slice(site.start, site.end);
|
|
958
|
+
// A statement that only lists or prints tests is not a run (`--list`, `--help`).
|
|
959
|
+
if (!isPlaywrightRunCommand(raw))
|
|
960
|
+
continue;
|
|
961
|
+
const reporters = lastReporterFlag(raw);
|
|
962
|
+
if (reporters === null) {
|
|
963
|
+
const insertAt = statementArgumentEnd(out, site.start, site.end);
|
|
964
|
+
if (insertAt === null)
|
|
965
|
+
continue;
|
|
966
|
+
out = `${out.slice(0, insertAt)} --reporter=list${out.slice(insertAt)}`;
|
|
967
|
+
rewrites.push({ kind: 'added', from: '' });
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const printing = reporters.values.filter((value) => PRINTING_REPORTERS.has(value));
|
|
971
|
+
// A set with no printing reporter is the executor's own artifact (`json`, `html`, `junit`, or
|
|
972
|
+
// a reporter this build does not know) and is left exactly as it was.
|
|
973
|
+
if (printing.length === 0)
|
|
974
|
+
continue;
|
|
975
|
+
if (printing.length === 1 && printing[0] === 'list')
|
|
976
|
+
continue;
|
|
977
|
+
let placed = false;
|
|
978
|
+
const kept = reporters.values
|
|
979
|
+
.map((value) => {
|
|
980
|
+
if (!PRINTING_REPORTERS.has(value))
|
|
981
|
+
return value;
|
|
982
|
+
if (placed)
|
|
983
|
+
return null;
|
|
984
|
+
placed = true;
|
|
985
|
+
return 'list';
|
|
986
|
+
})
|
|
987
|
+
.filter((value) => value !== null);
|
|
988
|
+
out =
|
|
989
|
+
out.slice(0, site.start + reporters.start) +
|
|
990
|
+
`--reporter${reporters.separator}${kept.join(',')}` +
|
|
991
|
+
out.slice(site.start + reporters.end);
|
|
992
|
+
rewrites.push({
|
|
993
|
+
kind: printing.includes('list') ? 'dropped' : 'switched',
|
|
994
|
+
from: reporters.values.join(','),
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const changes = [];
|
|
998
|
+
const count = (kind) => rewrites.filter((rewrite) => rewrite.kind === kind).length;
|
|
999
|
+
if (count('added') > 0)
|
|
1000
|
+
changes.push(`Added \`--reporter=list\` to ${count('added') === 1 ? 'the earlier test run' : 'each earlier test run'} ` +
|
|
1001
|
+
'so each test result is written to the log as it finishes (a config reporter may print ' +
|
|
1002
|
+
'nothing until the end).');
|
|
1003
|
+
const switched = rewrites.find((rewrite) => rewrite.kind === 'switched');
|
|
1004
|
+
if (switched)
|
|
1005
|
+
changes.push(`Switched ${count('switched') === 1 ? 'the earlier test run’s' : 'each earlier test run’s'} ` +
|
|
1006
|
+
`reporters to \`--reporter=list\`: \`list\` writes each test result as it finishes and is ` +
|
|
1007
|
+
'the shape the platform reads back, and it is the only printing reporter — two of them ' +
|
|
1008
|
+
'write every result into the log twice (X1 flash-m83).');
|
|
1009
|
+
const dropped = rewrites.find((rewrite) => rewrite.kind === 'dropped');
|
|
1010
|
+
if (dropped)
|
|
1011
|
+
changes.push(`Dropped a second printing reporter from ` +
|
|
1012
|
+
`${count('dropped') === 1 ? 'the earlier test run’s' : 'each earlier test run’s'} ` +
|
|
1013
|
+
`\`--reporter=${dropped.from}\`: two reporters that both print each failure write every ` +
|
|
1014
|
+
'test result into the log twice, so this run keeps `list` alone.');
|
|
1015
|
+
return { command: out, changes };
|
|
1016
|
+
}
|
|
868
1017
|
/**
|
|
869
1018
|
* Put Playwright's standard `json` report on a FOREGROUND run that already names its own reporters.
|
|
870
1019
|
*
|
|
@@ -1306,50 +1455,48 @@ function lastSuitePipeline(command) {
|
|
|
1306
1455
|
* @returns The located segments and the last suite stage's index among them, or null.
|
|
1307
1456
|
*/
|
|
1308
1457
|
function lastSuiteRun(command) {
|
|
1458
|
+
return suiteRunSites(command).at(-1) ?? null;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Every suite-run statement of a command, in command order, each with the parse's segments.
|
|
1462
|
+
*
|
|
1463
|
+
* A command may start the suite more than once — a whole-directory walk followed by a single
|
|
1464
|
+
* spec, or a Hurl run followed by a browser walk — and every one of them writes the log the
|
|
1465
|
+
* executor reads. Reading only the last one left the earlier walk's failing test's own error
|
|
1466
|
+
* behind a `| tail -5` while the LAST invocation was rewritten normally
|
|
1467
|
+
* (xr-molecule-flash-l21 +21:27: a 3.6-minute walk returned `72 passed` with no error body when
|
|
1468
|
+
* the driver ran `… specs 2>&1 | tail -5; echo START; … auth.spec.ts`).
|
|
1469
|
+
*
|
|
1470
|
+
* A command that runs no test at all is not a set of runs ({@link commandRunsNoTests}); nor is a
|
|
1471
|
+
* statement that only lists tests, which the reporter pass asks about per statement
|
|
1472
|
+
* ({@link isPlaywrightRunCommand}).
|
|
1473
|
+
*
|
|
1474
|
+
* @param command - The shell command.
|
|
1475
|
+
* @returns One entry per suite-running statement, all sharing the parse's segments.
|
|
1476
|
+
*/
|
|
1477
|
+
function suiteRunSites(command) {
|
|
1309
1478
|
if (commandRunsNoTests(command))
|
|
1310
|
-
return
|
|
1479
|
+
return [];
|
|
1311
1480
|
const located = locatedSegments(command);
|
|
1312
|
-
|
|
1481
|
+
const sites = [];
|
|
1313
1482
|
for (let i = 0; i < located.length; i += 1) {
|
|
1314
1483
|
const segment = located[i]?.segment;
|
|
1315
1484
|
if (segment && SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)))
|
|
1316
|
-
at
|
|
1485
|
+
sites.push({ located, at: i });
|
|
1317
1486
|
}
|
|
1318
|
-
return
|
|
1487
|
+
return sites;
|
|
1319
1488
|
}
|
|
1320
1489
|
/**
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
* A filter after the runner is not "the end of the log" the executor asked for: it hides the
|
|
1324
|
-
* failing test's own error (it usually matches none of `✘|passed|failed`), and the pipeline's exit
|
|
1325
|
-
* status becomes the FILTER's, so `… 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20` came back
|
|
1326
|
-
* `exitCode: 0` for a run with a failure in it (xr-molecule-flash-m101 +25:54). The tool returns
|
|
1327
|
-
* the end of the full log anyway, so the filter only costs diagnosability.
|
|
1328
|
-
*
|
|
1329
|
-
* Only a contiguous suffix of the stages after the runner is cut, and only stages the parse proves
|
|
1330
|
-
* are output-only ({@link isOutputOnlyFilter}): a `| tee` or `| xargs` keeps its place, and a
|
|
1331
|
-
* caller that reports the run's exit code must then treat it as unknown
|
|
1332
|
-
* ({@link suiteRunPipedAfterRunner}).
|
|
1333
|
-
*
|
|
1334
|
-
* A statement after the run's own pipeline (`; echo done`, `&& echo ok`) does not protect the
|
|
1335
|
-
* filters before it: the filter is thrown away and the trailing statement is kept, so the log the
|
|
1336
|
-
* run writes still holds every failure (xr-molecule-flash-m121 — a `| tail -5; echo done` kept its
|
|
1337
|
-
* tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
|
|
1338
|
-
* that command back).
|
|
1339
|
-
*
|
|
1340
|
-
* The command around the dropped stages is left as written: a group they sit in keeps its closer
|
|
1341
|
-
* (`( … | tail -25)` → `( … )`, never `( …` — xr-molecule-flash-m418 +23:31), and a stage the cut
|
|
1342
|
-
* would leave a closer dangling inside is not dropped at all.
|
|
1490
|
+
* The output-only filters piped after ONE suite runner, as a slice to remove — or null when this
|
|
1491
|
+
* runner has none this reader can prove output-only.
|
|
1343
1492
|
*
|
|
1344
1493
|
* @param command - The shell command.
|
|
1345
|
-
* @
|
|
1494
|
+
* @param located - The parse's located segments ({@link locatedSegments}).
|
|
1495
|
+
* @param at - The runner's index among them.
|
|
1496
|
+
* @returns The slice to drop, or null.
|
|
1346
1497
|
*/
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
if (run === null)
|
|
1350
|
-
return { command, dropped: '' };
|
|
1351
|
-
const { located, at } = run;
|
|
1352
|
-
// Where the suite's own pipeline ends: the first stage after the runner that is not joined to
|
|
1498
|
+
function suiteFilterCut(command, located, at) {
|
|
1499
|
+
// Where this suite's own pipeline ends: the first stage after the runner that is not joined to
|
|
1353
1500
|
// its predecessor by a single `|`. Everything from there on is another statement, kept as it is.
|
|
1354
1501
|
let end = at;
|
|
1355
1502
|
for (let i = at + 1; i < located.length; i += 1) {
|
|
@@ -1365,15 +1512,15 @@ export function stripTrailingSuiteFilters(command) {
|
|
|
1365
1512
|
first = i;
|
|
1366
1513
|
}
|
|
1367
1514
|
if (first === end + 1)
|
|
1368
|
-
return
|
|
1515
|
+
return null;
|
|
1369
1516
|
const previous = located[first - 1];
|
|
1370
1517
|
const stage = located[first];
|
|
1371
1518
|
const lastDropped = located[end];
|
|
1372
1519
|
if (!previous || !stage || !lastDropped)
|
|
1373
|
-
return
|
|
1520
|
+
return null;
|
|
1374
1521
|
// A stage inside a nested `sh -c` string has no offsets in this command: nothing can be cut.
|
|
1375
1522
|
if (previous.end === null || stage.start === null || lastDropped.end === null)
|
|
1376
|
-
return
|
|
1523
|
+
return null;
|
|
1377
1524
|
// Every stage dropped must be provably whole inside the command's own structure. A group
|
|
1378
1525
|
// closer attached to one of them closes a group the pipeline below still runs in
|
|
1379
1526
|
// (`( … | tail -5) | cat`), and a stage that OPENS a group it does not close (`| { cat; }`) has
|
|
@@ -1383,17 +1530,17 @@ export function stripTrailingSuiteFilters(command) {
|
|
|
1383
1530
|
for (let i = first; i <= end; i += 1) {
|
|
1384
1531
|
const droppedStage = located[i];
|
|
1385
1532
|
if (!droppedStage || droppedStage.end === null)
|
|
1386
|
-
return
|
|
1533
|
+
return null;
|
|
1387
1534
|
const boundary = statementBoundary(command, droppedStage.start, droppedStage.end);
|
|
1388
1535
|
if (boundary === null || boundary.leavesGroupOpen)
|
|
1389
|
-
return
|
|
1536
|
+
return null;
|
|
1390
1537
|
if (i < end && boundary.textEnd !== droppedStage.end)
|
|
1391
|
-
return
|
|
1538
|
+
return null;
|
|
1392
1539
|
boundaries.push(boundary);
|
|
1393
1540
|
}
|
|
1394
1541
|
const lastBoundary = boundaries[boundaries.length - 1];
|
|
1395
1542
|
if (lastBoundary === undefined)
|
|
1396
|
-
return
|
|
1543
|
+
return null;
|
|
1397
1544
|
// Where the dropped stages' own text ends. The tokenizer attaches the closer of an enclosing
|
|
1398
1545
|
// group to the stage's last word (`tail -25)`), so cutting at {@link LocatedSegment.end} deleted
|
|
1399
1546
|
// the `)` and left `( … 2>&1` — a command no shell parses, which the background runner rejected
|
|
@@ -1401,14 +1548,71 @@ export function stripTrailingSuiteFilters(command) {
|
|
|
1401
1548
|
const dropEnd = lastBoundary.textEnd;
|
|
1402
1549
|
const pipe = stage.between.indexOf('|');
|
|
1403
1550
|
if (pipe < 0)
|
|
1404
|
-
return
|
|
1551
|
+
return null;
|
|
1405
1552
|
const cut = previous.end + pipe;
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1553
|
+
return { cut, dropEnd, dropped: command.slice(cut, dropEnd).trim() };
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Drop the output-only filters piped after EVERY suite runner in the command.
|
|
1557
|
+
*
|
|
1558
|
+
* A filter after a runner is not "the end of the log" the executor asked for: it hides the
|
|
1559
|
+
* failing test's own error (it usually matches none of `✘|passed|failed`), and the pipeline's exit
|
|
1560
|
+
* status becomes the FILTER's, so `… 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20` came back
|
|
1561
|
+
* `exitCode: 0` for a run with a failure in it (xr-molecule-flash-m101 +25:54). The tool returns
|
|
1562
|
+
* the end of the full log anyway, so the filter only costs diagnosability.
|
|
1563
|
+
*
|
|
1564
|
+
* EVERY run is stripped, not only the command's last: a compound command's earlier walk writes
|
|
1565
|
+
* the same log, and its `| tail -5` threw away the error body of a 3.6-minute Playwright walk
|
|
1566
|
+
* whose two failure names were all the executor got back (xr-molecule-flash-l21 +21:27). Only a
|
|
1567
|
+
* run the parse places is touched, and only its own pipeline — a later statement's filters belong
|
|
1568
|
+
* to it.
|
|
1569
|
+
*
|
|
1570
|
+
* Only a contiguous suffix of the stages after the runner is cut, and only stages the parse proves
|
|
1571
|
+
* are output-only ({@link isOutputOnlyFilter}): a `| tee` or `| xargs` keeps its place, and a
|
|
1572
|
+
* caller that reports the run's exit code must then treat it as unknown
|
|
1573
|
+
* ({@link suiteRunPipedAfterRunner}).
|
|
1574
|
+
*
|
|
1575
|
+
* A statement after the run's own pipeline (`; echo done`, `&& echo ok`) does not protect the
|
|
1576
|
+
* filters before it: the filter is thrown away and the trailing statement is kept, so the log the
|
|
1577
|
+
* run writes still holds every failure (xr-molecule-flash-m121 — a `| tail -5; echo done` kept its
|
|
1578
|
+
* tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
|
|
1579
|
+
* that command back).
|
|
1580
|
+
*
|
|
1581
|
+
* The command around the dropped stages is left as written: a group they sit in keeps its closer
|
|
1582
|
+
* (`( … | tail -25)` → `( … )`, never `( …` — xr-molecule-flash-m418 +23:31), and a stage the cut
|
|
1583
|
+
* would leave a closer dangling inside is not dropped at all.
|
|
1584
|
+
*
|
|
1585
|
+
* @param command - The shell command.
|
|
1586
|
+
* @returns The rewritten command and the stages dropped, one entry per suite run, in command order.
|
|
1587
|
+
*/
|
|
1588
|
+
function suiteFilterDrops(command) {
|
|
1589
|
+
const cuts = [];
|
|
1590
|
+
for (const site of suiteRunSites(command)) {
|
|
1591
|
+
const cut = suiteFilterCut(command, site.located, site.at);
|
|
1592
|
+
if (cut !== null)
|
|
1593
|
+
cuts.push(cut);
|
|
1594
|
+
}
|
|
1595
|
+
if (cuts.length === 0)
|
|
1596
|
+
return { command, drops: [] };
|
|
1597
|
+
// Right to left, so every cut's offsets still describe the text it was measured on.
|
|
1598
|
+
let out = command;
|
|
1599
|
+
for (const cut of [...cuts].sort((a, b) => b.cut - a.cut)) {
|
|
1600
|
+
// The stages dropped are a middle slice when a statement follows, so the text after them is
|
|
1601
|
+
// kept — `; echo done` still runs, and so does the punctuation closing the group they sat in.
|
|
1602
|
+
out = `${out.slice(0, cut.cut).trimEnd()}${out.slice(cut.dropEnd)}`;
|
|
1603
|
+
}
|
|
1604
|
+
return { command: out, drops: cuts.map((cut) => cut.dropped) };
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Drop the output-only filters piped after the command's suite runners
|
|
1608
|
+
* ({@link suiteFilterDrops}).
|
|
1609
|
+
*
|
|
1610
|
+
* @param command - The shell command.
|
|
1611
|
+
* @returns The rewritten command, and the stages dropped in command order (`''` when none were).
|
|
1612
|
+
*/
|
|
1613
|
+
export function stripTrailingSuiteFilters(command) {
|
|
1614
|
+
const { command: out, drops } = suiteFilterDrops(command);
|
|
1615
|
+
return { command: out, dropped: drops.join(' ') };
|
|
1412
1616
|
}
|
|
1413
1617
|
/**
|
|
1414
1618
|
* Where the suite runner's exit code comes from: the runner itself, or a stage piped after it.
|
|
@@ -2423,6 +2627,43 @@ export function runningCauseCommand(log, own) {
|
|
|
2423
2627
|
const read = `cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null`;
|
|
2424
2628
|
return `${read} | tail -c ${REPEATING_CAUSE_TAIL_BYTES}`;
|
|
2425
2629
|
}
|
|
2630
|
+
/** Bytes of a running log a diagnostic note reads back: one {@link REPEATING_CAUSE_TAIL_BYTES} window. */
|
|
2631
|
+
export const RUNNING_LOG_READ_BYTES = REPEATING_CAUSE_TAIL_BYTES;
|
|
2632
|
+
/**
|
|
2633
|
+
* The shell command whose stdout is a bounded CONTIGUOUS read of a running task's own log: the
|
|
2634
|
+
* log's byte size, then its last {@link RUNNING_LOG_READ_BYTES} bytes.
|
|
2635
|
+
*
|
|
2636
|
+
* {@link runningOutputSliceCommand} is the wrong read for a diagnosis, and l07 is the finding: it
|
|
2637
|
+
* stitches the first 40 grep-selected failure lines to the last 1 000 bytes with a blank line
|
|
2638
|
+
* between, so the tail is DISJOINT from the headers above it. Read as one log, the last numbered
|
|
2639
|
+
* header absorbs whatever the tail starts with — a clipped skipped-test title became the cause of
|
|
2640
|
+
* a genuine failure (xr-molecule-flash-l07). A contiguous window can only ever hold real
|
|
2641
|
+
* neighbouring lines of one log.
|
|
2642
|
+
*
|
|
2643
|
+
* The size rides along so a caller can say when the window is only the log's end
|
|
2644
|
+
* ({@link parseRunningLogRead}); without it a tail reads as the whole run.
|
|
2645
|
+
*
|
|
2646
|
+
* @param log - The task log path.
|
|
2647
|
+
* @param own - A file the command redirects its own output to, if any.
|
|
2648
|
+
* @returns The shell command, safe to hand to the execution backend as-is.
|
|
2649
|
+
*/
|
|
2650
|
+
export function runningLogReadCommand(log, own) {
|
|
2651
|
+
const read = `cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null`;
|
|
2652
|
+
return `{ ${read} | wc -c; ${read} | tail -c ${RUNNING_LOG_READ_BYTES}; }`;
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* The two parts of {@link runningLogReadCommand}'s answer.
|
|
2656
|
+
*
|
|
2657
|
+
* @param stdout - The command's stdout.
|
|
2658
|
+
* @returns The window and the whole log's byte size, or null when the size line is absent (the
|
|
2659
|
+
* read did not run, so nothing may be said about the log).
|
|
2660
|
+
*/
|
|
2661
|
+
export function parseRunningLogRead(stdout) {
|
|
2662
|
+
const sized = /^\s*(\d+)\s*\n/.exec(stdout);
|
|
2663
|
+
if (sized === null)
|
|
2664
|
+
return null;
|
|
2665
|
+
return { text: stdout.slice(sized[0].length), bytes: Number(sized[1]) };
|
|
2666
|
+
}
|
|
2426
2667
|
/**
|
|
2427
2668
|
* DISTINCT tests that must share one signature before a running suite's repeating cause is named.
|
|
2428
2669
|
* A test the log prints twice, or a test that failed twice, is still one test.
|
|
@@ -2513,6 +2754,12 @@ function failureBody(block) {
|
|
|
2513
2754
|
.map((l) => l.replace(ANSI_SGR, '').trim())
|
|
2514
2755
|
.filter((l) => l &&
|
|
2515
2756
|
!LIST_RESULT_LINE.test(l) &&
|
|
2757
|
+
// A SKIPPED test's own row (` - 41 [chromium] › user-fetch-errors.spec.ts:88:3 › …`) is
|
|
2758
|
+
// a progress line, never a failure's body: a block with no body of its own is followed by
|
|
2759
|
+
// whatever the reporter printed next, and that row became the failure's "cause" when the
|
|
2760
|
+
// wait's stitched display was read as one log (xr-molecule-flash-l07). A `- waiting for`
|
|
2761
|
+
// call-log line stays — it names the locator, and the dash carries no index.
|
|
2762
|
+
!/^\s*-\s+\d+\s+/.test(l) &&
|
|
2516
2763
|
!/^(?:at\s|\d+ \||>\s*\d+ \||\|)/.test(l) &&
|
|
2517
2764
|
!/^Error Context:/.test(l));
|
|
2518
2765
|
}
|
|
@@ -3870,14 +4117,14 @@ regressedSpecs = []) {
|
|
|
3870
4117
|
'handler shape, a header or CORS.');
|
|
3871
4118
|
}
|
|
3872
4119
|
// A text that is the TAIL of a longer log says so in its first line (`…(N earlier characters
|
|
3873
|
-
// omitted)`,
|
|
4120
|
+
// omitted)`, or bytes for a byte-bounded read): what it holds is not
|
|
3874
4121
|
// the run's list of failures, only the part that survived the cut. Saying "N failures in this
|
|
3875
4122
|
// run" over it reads as complete, and did: xr-molecule-flash-m123's collected 42 KB diagnosis
|
|
3876
4123
|
// log was clipped to its last 8 KB, and this note grouped 2 of the run's 14 failures under
|
|
3877
4124
|
// "fix every cause below" while the 11 that shared the run's one real cause were gone
|
|
3878
4125
|
// (2026-10-17). The root fix is to hand this function the whole log; this says so when a
|
|
3879
4126
|
// caller cannot.
|
|
3880
|
-
const clipped = /^…\((\d+) earlier characters omitted\)/.exec(log);
|
|
4127
|
+
const clipped = /^…\((\d+) earlier (characters|bytes) omitted\)/.exec(log);
|
|
3881
4128
|
// The missing-header reading leads the list: four unrelated locators failing while every
|
|
3882
4129
|
// snapshot shows the page painted and un-navigated is ONE fact about the route, and the list
|
|
3883
4130
|
// below would otherwise read as four separate defects (xr-molecule-flash-m193 rewrote the
|
|
@@ -3887,7 +4134,7 @@ regressedSpecs = []) {
|
|
|
3887
4134
|
const header = (missingHeader === null ? '' : `${missingHeader}\n`) +
|
|
3888
4135
|
(clipped !== null
|
|
3889
4136
|
? `${total} failure${total === 1 ? '' : 's'} in the LAST part of this run’s log — its ` +
|
|
3890
|
-
`first ${clipped[1]}
|
|
4137
|
+
`first ${clipped[1]} ${clipped[2]} were clipped, so a cause that failed earlier is ` +
|
|
3891
4138
|
`missing here. Fix every cause below${connections.length > 0 ? ' the app’s code can produce, and re-send the connection error once as it is' : ''}, ` +
|
|
3892
4139
|
`and read the whole run log before concluding:\n`
|
|
3893
4140
|
: `${total} failure${total === 1 ? '' : 's'} in this run, grouped by cause — fix every cause ` +
|