@molecule/api-ai-tools 1.9.2 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -28
- package/dist/suite-overlap.d.ts +7 -4
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +154 -7
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +81 -17
- package/dist/trace-artifacts.d.ts +40 -0
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +169 -10
- package/dist/utilities.d.ts +83 -21
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +289 -48
- package/package.json +1 -1
package/dist/utilities.js
CHANGED
|
@@ -679,6 +679,31 @@ function isOutputOnlyFilter(segment) {
|
|
|
679
679
|
* @returns The located segments and the last suite stage's index among them, or null.
|
|
680
680
|
*/
|
|
681
681
|
function lastSuitePipeline(command) {
|
|
682
|
+
const run = lastSuiteRun(command);
|
|
683
|
+
if (run === null)
|
|
684
|
+
return null;
|
|
685
|
+
for (let i = run.at + 1; i < run.located.length; i += 1) {
|
|
686
|
+
if ((run.located[i]?.between ?? '').trim() !== '|')
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
return run;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* The command's last suite stage, and the parse's segments around it — whatever follows that
|
|
693
|
+
* statement.
|
|
694
|
+
*
|
|
695
|
+
* {@link lastSuitePipeline} refuses a suite that is not the command's last step, because the code
|
|
696
|
+
* the command exits with is then the following statement's. That is the right answer to "whose
|
|
697
|
+
* exit code is this", and the wrong one to "which stages filter this run's output": a
|
|
698
|
+
* `… --reporter=list 2>&1 | tail -5; echo done` still pipes its log through a tail, and the tail
|
|
699
|
+
* still throws away every failure line the run printed (xr-molecule-flash-m121 +09:14, where the
|
|
700
|
+
* platform's own hold handed that shape back and the first 95-second walk's log held only the
|
|
701
|
+
* tally).
|
|
702
|
+
*
|
|
703
|
+
* @param command - The shell command.
|
|
704
|
+
* @returns The located segments and the last suite stage's index among them, or null.
|
|
705
|
+
*/
|
|
706
|
+
function lastSuiteRun(command) {
|
|
682
707
|
if (commandRunsNoTests(command))
|
|
683
708
|
return null;
|
|
684
709
|
const located = locatedSegments(command);
|
|
@@ -688,13 +713,7 @@ function lastSuitePipeline(command) {
|
|
|
688
713
|
if (segment && SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)))
|
|
689
714
|
at = i;
|
|
690
715
|
}
|
|
691
|
-
|
|
692
|
-
return null;
|
|
693
|
-
for (let i = at + 1; i < located.length; i += 1) {
|
|
694
|
-
if ((located[i]?.between ?? '').trim() !== '|')
|
|
695
|
-
return null;
|
|
696
|
-
}
|
|
697
|
-
return { located, at };
|
|
716
|
+
return at < 0 ? null : { located, at };
|
|
698
717
|
}
|
|
699
718
|
/**
|
|
700
719
|
* Drop the output-only filters piped after the command's suite runner.
|
|
@@ -710,35 +729,55 @@ function lastSuitePipeline(command) {
|
|
|
710
729
|
* caller that reports the run's exit code must then treat it as unknown
|
|
711
730
|
* ({@link suiteRunPipedAfterRunner}).
|
|
712
731
|
*
|
|
732
|
+
* A statement after the run's own pipeline (`; echo done`, `&& echo ok`) does not protect the
|
|
733
|
+
* filters before it: the filter is thrown away and the trailing statement is kept, so the log the
|
|
734
|
+
* run writes still holds every failure (xr-molecule-flash-m121 — a `| tail -5; echo done` kept its
|
|
735
|
+
* tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
|
|
736
|
+
* that command back).
|
|
737
|
+
*
|
|
713
738
|
* @param command - The shell command.
|
|
714
739
|
* @returns The rewritten command and the stages dropped.
|
|
715
740
|
*/
|
|
716
741
|
export function stripTrailingSuiteFilters(command) {
|
|
717
|
-
const
|
|
718
|
-
if (
|
|
742
|
+
const run = lastSuiteRun(command);
|
|
743
|
+
if (run === null)
|
|
719
744
|
return { command, dropped: '' };
|
|
720
|
-
const { located, at } =
|
|
721
|
-
|
|
722
|
-
|
|
745
|
+
const { located, at } = run;
|
|
746
|
+
// Where the suite's own pipeline ends: the first stage after the runner that is not joined to
|
|
747
|
+
// its predecessor by a single `|`. Everything from there on is another statement, kept as it is.
|
|
748
|
+
let end = at;
|
|
749
|
+
for (let i = at + 1; i < located.length; i += 1) {
|
|
750
|
+
if ((located[i]?.between ?? '').trim() !== '|')
|
|
751
|
+
break;
|
|
752
|
+
end = i;
|
|
753
|
+
}
|
|
754
|
+
let first = end + 1;
|
|
755
|
+
for (let i = end; i > at; i -= 1) {
|
|
723
756
|
const stage = located[i];
|
|
724
757
|
if (!stage || !isOutputOnlyFilter(stage.segment))
|
|
725
758
|
break;
|
|
726
759
|
first = i;
|
|
727
760
|
}
|
|
728
|
-
if (first ===
|
|
761
|
+
if (first === end + 1)
|
|
729
762
|
return { command, dropped: '' };
|
|
730
763
|
const previous = located[first - 1];
|
|
731
764
|
const stage = located[first];
|
|
732
|
-
|
|
765
|
+
const lastDropped = located[end];
|
|
766
|
+
if (!previous || !stage || !lastDropped)
|
|
733
767
|
return { command, dropped: '' };
|
|
734
768
|
// A stage inside a nested `sh -c` string has no offsets in this command: nothing can be cut.
|
|
735
|
-
if (previous.end === null || stage.start === null)
|
|
769
|
+
if (previous.end === null || stage.start === null || lastDropped.end === null)
|
|
736
770
|
return { command, dropped: '' };
|
|
737
771
|
const pipe = stage.between.indexOf('|');
|
|
738
772
|
if (pipe < 0)
|
|
739
773
|
return { command, dropped: '' };
|
|
740
774
|
const cut = previous.end + pipe;
|
|
741
|
-
|
|
775
|
+
// The stages dropped are a middle slice when a statement follows, so the text after them is
|
|
776
|
+
// kept — `; echo done` still runs.
|
|
777
|
+
return {
|
|
778
|
+
command: `${command.slice(0, cut).trimEnd()}${command.slice(lastDropped.end)}`,
|
|
779
|
+
dropped: command.slice(cut, lastDropped.end).trim(),
|
|
780
|
+
};
|
|
742
781
|
}
|
|
743
782
|
/**
|
|
744
783
|
* Where the suite runner's exit code comes from: the runner itself, or a stage piped after it.
|
|
@@ -1744,6 +1783,17 @@ const FAILURE_EVIDENCE_CHARS = 200;
|
|
|
1744
1783
|
*/
|
|
1745
1784
|
function failureEvidence(body, message) {
|
|
1746
1785
|
const carried = (line) => message !== null && message.includes(line);
|
|
1786
|
+
// A strict-mode violation lists the elements it resolved to, and that list is the fix's own
|
|
1787
|
+
// address: `1) <button class="… action-btn">` and `2) <button class="…">` name where each
|
|
1788
|
+
// duplicate is written. They were dropped here (the first body line is the signature, and the
|
|
1789
|
+
// only later `Error:` line this read for is absent), so xr-molecule-flash-m122's run read
|
|
1790
|
+
// "resolved to 2 elements:" followed by nothing and fixed a duplicate on a page its failing
|
|
1791
|
+
// test never visits. Kept whole — a locator list is what the executor has to match to a file.
|
|
1792
|
+
if (message !== null && /resolved to \d+ elements?/i.test(message)) {
|
|
1793
|
+
const elements = body.filter((line) => /^\d+\)\s/.test(line));
|
|
1794
|
+
if (elements.length > 0)
|
|
1795
|
+
return elements.slice(0, 2);
|
|
1796
|
+
}
|
|
1747
1797
|
const expected = body.find((l) => /^Expected\b/.test(l));
|
|
1748
1798
|
const received = body.find((l) => /^Received\b/.test(l));
|
|
1749
1799
|
if (expected !== undefined && received !== undefined)
|
|
@@ -2051,29 +2101,135 @@ function failureBlockTests(text) {
|
|
|
2051
2101
|
* property of the run rather than of a page.
|
|
2052
2102
|
*/
|
|
2053
2103
|
export const UNIVERSAL_CAUSE_FILES = 3;
|
|
2104
|
+
/**
|
|
2105
|
+
* How many tests a connection-class cause must cover before the run is stopped even when they
|
|
2106
|
+
* are all in one file — see {@link universalCause}.
|
|
2107
|
+
*/
|
|
2108
|
+
export const CONNECTION_CAUSE_TESTS = 3;
|
|
2109
|
+
/**
|
|
2110
|
+
* The failures that mean the browser never reached the app: neither the page's code nor a
|
|
2111
|
+
* selector can be the cause, and every remaining test can only repeat it.
|
|
2112
|
+
*/
|
|
2113
|
+
const CONNECTION_CAUSE = /\bpage\.goto\b[^\n]*\btimeout\b|\bnet::ERR_[A-Z0-9_]+\b|\bECONNREFUSED\b/i;
|
|
2114
|
+
/** A Playwright positional argument that names one test file (`e2e/auth.spec.ts:19:3`). */
|
|
2115
|
+
const SPEC_FILE_ARG = /\.(?:spec|test)\.[cm]?[jt]sx?(?::\d+(?::\d+)?)?$/;
|
|
2116
|
+
/**
|
|
2117
|
+
* The `playwright test` flags that take the next word as their value when they are not written
|
|
2118
|
+
* `--flag=value`. A flag NOT in this set is a switch (`--headed`, `--last-failed`), so the word
|
|
2119
|
+
* after it is a positional filter and not a value.
|
|
2120
|
+
*/
|
|
2121
|
+
const PLAYWRIGHT_VALUE_FLAGS = new Set([
|
|
2122
|
+
'-c',
|
|
2123
|
+
'-g',
|
|
2124
|
+
'-j',
|
|
2125
|
+
'-x',
|
|
2126
|
+
'--config',
|
|
2127
|
+
'--global-timeout',
|
|
2128
|
+
'--grep',
|
|
2129
|
+
'--grep-invert',
|
|
2130
|
+
'--max-failures',
|
|
2131
|
+
'--output',
|
|
2132
|
+
'--project',
|
|
2133
|
+
'--repeat-each',
|
|
2134
|
+
'--reporter',
|
|
2135
|
+
'--retries',
|
|
2136
|
+
'--shard',
|
|
2137
|
+
'--timeout',
|
|
2138
|
+
'--trace',
|
|
2139
|
+
'--ui-host',
|
|
2140
|
+
'--ui-port',
|
|
2141
|
+
'--workers',
|
|
2142
|
+
]);
|
|
2143
|
+
/**
|
|
2144
|
+
* How many test FILES a Playwright run's own command narrows it to, or null when it selects the
|
|
2145
|
+
* whole suite (or its selection cannot be read).
|
|
2146
|
+
*
|
|
2147
|
+
* {@link universalCause}'s file threshold is written for a whole-suite run, which a narrowed run
|
|
2148
|
+
* can never reach. xr-molecule-flash-m119 (+42:08) re-ran ONE spec file against a hand-started
|
|
2149
|
+
* `vite preview` the browser could not reach, and every test in it failed `page.goto: Timeout` —
|
|
2150
|
+
* but one file is below {@link UNIVERSAL_CAUSE_FILES}, so the wait sat out its entire 280 s
|
|
2151
|
+
* budget on a run that had already measured all it could.
|
|
2152
|
+
*
|
|
2153
|
+
* The command is read through {@link commandSegments}, so a `playwright test` named as an
|
|
2154
|
+
* argument is not a run. A {@link PLAYWRIGHT_VALUE_FLAGS} flag written without `=` takes the next
|
|
2155
|
+
* word as its value, so a `--config app/e2e-suite.config.ts` is never counted as a spec file; a
|
|
2156
|
+
* directory or a bare substring filter names files the command does not list, which answers null
|
|
2157
|
+
* (the whole-suite threshold) rather than a count of the wrong files.
|
|
2158
|
+
*
|
|
2159
|
+
* @param command - The run's command.
|
|
2160
|
+
* @returns The number of distinct test files it names, or null when it names none (the whole
|
|
2161
|
+
* suite) or the selection cannot be told.
|
|
2162
|
+
*/
|
|
2163
|
+
export function specFilesSelected(command) {
|
|
2164
|
+
for (const segment of commandSegments(command)) {
|
|
2165
|
+
if (!/^playwright\s+test\b/.test(segment.text))
|
|
2166
|
+
continue;
|
|
2167
|
+
const files = new Set();
|
|
2168
|
+
let flagValue = false;
|
|
2169
|
+
for (const word of segment.args.slice(1)) {
|
|
2170
|
+
const token = word.replace(/^(['"])(.*)\1$/, '$2');
|
|
2171
|
+
if (flagValue) {
|
|
2172
|
+
flagValue = false;
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
if (token.startsWith('-')) {
|
|
2176
|
+
const flag = token.includes('=') ? token.slice(0, token.indexOf('=')) : token;
|
|
2177
|
+
flagValue = !token.includes('=') && PLAYWRIGHT_VALUE_FLAGS.has(flag);
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
if (SPEC_FILE_ARG.test(token)) {
|
|
2181
|
+
files.add(token.replace(/:\d+(?::\d+)?$/, ''));
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
// A directory (`e2e/`), a glob, or a bare substring filter (`npx playwright test login`):
|
|
2185
|
+
// the command does not name the files it selects, so there is no count to take.
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
return files.size > 0 ? files.size : null;
|
|
2189
|
+
}
|
|
2190
|
+
return null;
|
|
2191
|
+
}
|
|
2054
2192
|
/**
|
|
2055
2193
|
* Whether a repeating cause is failing the whole run — the one case where stopping it is the
|
|
2056
2194
|
* right call.
|
|
2057
2195
|
*
|
|
2058
|
-
* The test is: the cause accounts for every failure the log names, it has hit
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
2064
|
-
*
|
|
2065
|
-
*
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2196
|
+
* The test is: the cause accounts for every failure the log names, it has hit the spec-file
|
|
2197
|
+
* threshold, and nothing has passed since its first failure. The threshold is
|
|
2198
|
+
* {@link UNIVERSAL_CAUSE_FILES} for a whole-suite run but the number of files the command
|
|
2199
|
+
* SELECTED for a narrowed one ({@link specFilesSelected}): a one-file run can never span three,
|
|
2200
|
+
* and xr-molecule-flash-m119 (+42:08) waited out its whole budget on exactly that shape. An app
|
|
2201
|
+
* that is not up, a 5xx on every request, or a selector that is missing on every page all look
|
|
2202
|
+
* like that, and every remaining test will fail the same way — so the run's remaining minutes
|
|
2203
|
+
* measure nothing. A CONNECTION-class cause ({@link CONNECTION_CAUSE}) needs no file span at all:
|
|
2204
|
+
* three tests that could not reach the app, with nothing passing since, is the app-unreachable
|
|
2205
|
+
* case however few files the run walks.
|
|
2206
|
+
*
|
|
2207
|
+
* Anything less is NOT this: a cause that stops at one file of a multi-file run, or that leaves
|
|
2208
|
+
* other tests passing, or that shares the run with failures of another shape, leaves the run with
|
|
2209
|
+
* something to say, and the run is the only thing that will say it. X1 flash-m83 (2026-10-16)
|
|
2210
|
+
* stopped eight local runs at their first repeating cause over 14.5 minutes and never finished
|
|
2211
|
+
* one; three of its deploys each fixed a single cause that a finished run would have listed
|
|
2212
|
+
* alongside the others.
|
|
2068
2213
|
*
|
|
2069
2214
|
* @param log - The log so far.
|
|
2070
2215
|
* @param found - The repeating cause {@link repeatingFailure} found in it.
|
|
2216
|
+
* @param command - How the run was started, when the caller knows it: its spec-file selection
|
|
2217
|
+
* scales the file threshold ({@link specFilesSelected}).
|
|
2071
2218
|
* @returns The cause's count and files, or null when the run is not failing whole.
|
|
2072
2219
|
*/
|
|
2073
|
-
export function universalCause(log, found) {
|
|
2220
|
+
export function universalCause(log, found, command) {
|
|
2074
2221
|
const text = log.replace(ANSI_SGR, '');
|
|
2075
2222
|
const files = [...new Set(found.tests.map((t) => specFile(t.where)).filter(Boolean))];
|
|
2076
|
-
|
|
2223
|
+
// The threshold is what THIS run selects, not the suite's total: a run narrowed to one file
|
|
2224
|
+
// can never span three of them, however whole its failure is.
|
|
2225
|
+
const selected = command ? specFilesSelected(command) : null;
|
|
2226
|
+
const threshold = selected === null ? UNIVERSAL_CAUSE_FILES : Math.min(UNIVERSAL_CAUSE_FILES, selected);
|
|
2227
|
+
// The app never answered: three tests of that with nothing passing since is the whole of what
|
|
2228
|
+
// a whole-suite run would have said, whatever the file count (xr-molecule-flash-m119).
|
|
2229
|
+
const unreachable = found.message !== null &&
|
|
2230
|
+
CONNECTION_CAUSE.test(found.message) &&
|
|
2231
|
+
found.count >= CONNECTION_CAUSE_TESTS;
|
|
2232
|
+
if (!unreachable && files.length < threshold)
|
|
2077
2233
|
return null;
|
|
2078
2234
|
// Every failure the log names has to be this cause: another cause anywhere in the run means
|
|
2079
2235
|
// the run still has something to say, and stopping it throws that away.
|
|
@@ -2094,8 +2250,10 @@ export function universalCause(log, found) {
|
|
|
2094
2250
|
*
|
|
2095
2251
|
* Both halves are load-bearing, each of them learned from a run that lost minutes to the other.
|
|
2096
2252
|
*
|
|
2097
|
-
* - **Failing the WHOLE run** is {@link universalCause}: every failure the log names, across
|
|
2098
|
-
* {@link UNIVERSAL_CAUSE_FILES}
|
|
2253
|
+
* - **Failing the WHOLE run** is {@link universalCause}: every failure the log names, across the
|
|
2254
|
+
* spec-file threshold — {@link UNIVERSAL_CAUSE_FILES}, or the files a narrowed run's own
|
|
2255
|
+
* command selects, or {@link CONNECTION_CAUSE_TESTS} tests of an app that never answered — with
|
|
2256
|
+
* nothing passing since the first. That is the
|
|
2099
2257
|
* dont-stop-runs rule. X1 flash-m83 (2026-10-16) stopped eight local runs at their first
|
|
2100
2258
|
* repeating cause in 14.5 minutes and never finished one, and a run that still has other causes
|
|
2101
2259
|
* left to name is the only thing that will name them.
|
|
@@ -2236,7 +2394,7 @@ function flakyTestsDeclared(text) {
|
|
|
2236
2394
|
* @returns The causes to fix, the distinct tests dropped as excluded, and the distinct tests the
|
|
2237
2395
|
* runner counted as flaky (a retried pass is not a cause to fix).
|
|
2238
2396
|
*/
|
|
2239
|
-
function groupFailures(log, excluded) {
|
|
2397
|
+
function groupFailures(log, excluded, pageErrors = []) {
|
|
2240
2398
|
const text = log.replace(ANSI_SGR, '');
|
|
2241
2399
|
const flakyTests = flakyTestsDeclared(text);
|
|
2242
2400
|
const flaky = new Set(flakyTests.map(testKey));
|
|
@@ -2272,9 +2430,21 @@ function groupFailures(log, excluded) {
|
|
|
2272
2430
|
});
|
|
2273
2431
|
}
|
|
2274
2432
|
const causes = [];
|
|
2433
|
+
// The page's own words for a test, keyed the way both sides spell the location: the log's
|
|
2434
|
+
// `file:line` and the artifact's `file:line` are the same string from one run.
|
|
2435
|
+
const showedBy = new Map();
|
|
2436
|
+
for (const entry of pageErrors)
|
|
2437
|
+
if (entry.items.length > 0)
|
|
2438
|
+
showedBy.set(entry.where, entry.items);
|
|
2275
2439
|
for (const [message, group] of byMessage) {
|
|
2276
2440
|
const tests = distinctTests(group.tests);
|
|
2277
|
-
if (tests.length > 0)
|
|
2441
|
+
if (tests.length > 0) {
|
|
2442
|
+
// The first test of the cause that has page errors speaks for it: tests grouped under one
|
|
2443
|
+
// error were failing the same way, and every one of m122's eight snapshots carried the same
|
|
2444
|
+
// two blank-field alerts.
|
|
2445
|
+
const showed = tests
|
|
2446
|
+
.map((t) => showedBy.get(t.where))
|
|
2447
|
+
.find((items) => items !== undefined);
|
|
2278
2448
|
causes.push({
|
|
2279
2449
|
message,
|
|
2280
2450
|
seconds: null,
|
|
@@ -2284,7 +2454,9 @@ function groupFailures(log, excluded) {
|
|
|
2284
2454
|
// no layer of its own is read as an assertion (unknown is never `connection`).
|
|
2285
2455
|
kind: group.kind,
|
|
2286
2456
|
...(group.budgetMs === null ? {} : { budgetMs: group.budgetMs }),
|
|
2457
|
+
...(showed === undefined ? {} : { pageShowed: [...showed] }),
|
|
2287
2458
|
});
|
|
2459
|
+
}
|
|
2288
2460
|
}
|
|
2289
2461
|
// The failures with no body of their own — a `list` log with no error blocks in it, from a run
|
|
2290
2462
|
// not started in the background — are grouped by how long they took: a band is opened by its
|
|
@@ -2329,6 +2501,20 @@ function groupFailures(log, excluded) {
|
|
|
2329
2501
|
* xr-molecule-flash-m113 (+20:34) got ten of these and a list that called them ten causes.
|
|
2330
2502
|
*/
|
|
2331
2503
|
const NO_BASE_URL_ERROR = /Cannot navigate to invalid URL/i;
|
|
2504
|
+
/** The browser's own `net::ERR_*`, as Playwright prints it for a document that never loaded. */
|
|
2505
|
+
const NETWORK_ERROR_TEXT = /\bnet::ERR_[A-Z0-9_]+\b/;
|
|
2506
|
+
/**
|
|
2507
|
+
* Whether every cause this run grouped is a `page.goto` that ran out of time.
|
|
2508
|
+
*
|
|
2509
|
+
* @param causes - The run's grouped causes.
|
|
2510
|
+
* @returns True when the run measured no page at all.
|
|
2511
|
+
*/
|
|
2512
|
+
function everyCauseIsNavigationTimeout(causes) {
|
|
2513
|
+
return (causes.length > 0 &&
|
|
2514
|
+
causes.every((cause) => cause.message !== null &&
|
|
2515
|
+
(/\bpage\.goto\b/.test(cause.message) || /navigating to/.test(cause.message)) &&
|
|
2516
|
+
/timeout|timed out/i.test(cause.message)));
|
|
2517
|
+
}
|
|
2332
2518
|
/**
|
|
2333
2519
|
* The one-line note for a run whose EVERY failure is {@link NO_BASE_URL_ERROR}: a configuration
|
|
2334
2520
|
* failure, not a result about the app.
|
|
@@ -2390,8 +2576,8 @@ function noBaseUrlConfigNote(causes, run, excluded) {
|
|
|
2390
2576
|
* "left out" sentence greps when more causes exist than the result has room for.
|
|
2391
2577
|
* @returns The note, or null.
|
|
2392
2578
|
*/
|
|
2393
|
-
export function failureCausesNote(log, excluded = [], run) {
|
|
2394
|
-
const { causes, ignored, flaky } = groupFailures(log, excluded);
|
|
2579
|
+
export function failureCausesNote(log, excluded = [], run, pageErrors = []) {
|
|
2580
|
+
const { causes, ignored, flaky } = groupFailures(log, excluded, pageErrors);
|
|
2395
2581
|
const total = causes.reduce((n, c) => n + c.tests.length, 0);
|
|
2396
2582
|
if (total < 2)
|
|
2397
2583
|
return null;
|
|
@@ -2406,6 +2592,23 @@ export function failureCausesNote(log, excluded = [], run) {
|
|
|
2406
2592
|
// cannot report them again.
|
|
2407
2593
|
const locations = [...new Set(causes.flatMap((c) => c.tests.map((t) => t.where)).filter(Boolean))];
|
|
2408
2594
|
const rerun = run ? rerunOfRunningSuite(run, locations, excluded) : null;
|
|
2595
|
+
// A document the browser could not load explains every failure at once, and it is not in the
|
|
2596
|
+
// app: m118 (+37:09) read 14 `page.goto` timeouts as app failures while the page load had ended
|
|
2597
|
+
// `net::ERR_CONTENT_DECODING_FAILED`. The cause list is the wrong shape here — there is one
|
|
2598
|
+
// cause and it is the server — so the note leads with the browser's own line, not a fix round.
|
|
2599
|
+
const networkError = NETWORK_ERROR_TEXT.exec(log)?.[0];
|
|
2600
|
+
if (networkError !== undefined && everyCauseIsNavigationTimeout(causes)) {
|
|
2601
|
+
return (`${total} failure${total === 1 ? '' : 's'} in this run, and every one is a \`page.goto\` ` +
|
|
2602
|
+
`that never loaded while the browser reported \`${networkError}\` — the document request ` +
|
|
2603
|
+
`itself failed, so none of these is the app’s code or a selector. Fix the server first` +
|
|
2604
|
+
(networkError === 'net::ERR_CONTENT_DECODING_FAILED'
|
|
2605
|
+
? ': a body whose gzip `content-encoding` no longer matches its bytes does this, and a ' +
|
|
2606
|
+
'`vite preview` over a build must be started with ' +
|
|
2607
|
+
'`MOL_E2E_PREVIEW_PLUGIN=0 npx vite preview --port 4173 --strictPort`'
|
|
2608
|
+
: ' (what the run hit, and why it refused the connection)') +
|
|
2609
|
+
(rerun ? `. Then send exactly this: \`${rerun}\`` : '') +
|
|
2610
|
+
(locations.length > 0 ? `. Failing tests: ${locations.join(', ')}.` : ''));
|
|
2611
|
+
}
|
|
2409
2612
|
const ignoredTitles = [...new Set(ignored.map((t) => t.title || t.where).filter(Boolean))];
|
|
2410
2613
|
const flakyTitles = [...new Set(flaky.map((t) => t.where || t.title).filter(Boolean))];
|
|
2411
2614
|
// A cause that never reached the app is not a cause the app's code can fix: the transport
|
|
@@ -2421,8 +2624,22 @@ export function failureCausesNote(log, excluded = [], run) {
|
|
|
2421
2624
|
"if the failure repeats, read the app's own log (`read_logs`) — a connection error is never a " +
|
|
2422
2625
|
'handler shape, a header or CORS.');
|
|
2423
2626
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2627
|
+
// A text that is the TAIL of a longer log says so in its first line (`…(N earlier characters
|
|
2628
|
+
// omitted)`, the shape `background-tasks` writes when a log is capped): what it holds is not
|
|
2629
|
+
// the run's list of failures, only the part that survived the cut. Saying "N failures in this
|
|
2630
|
+
// run" over it reads as complete, and did: xr-molecule-flash-m123's collected 42 KB diagnosis
|
|
2631
|
+
// log was clipped to its last 8 KB, and this note grouped 2 of the run's 14 failures under
|
|
2632
|
+
// "fix every cause below" while the 11 that shared the run's one real cause were gone
|
|
2633
|
+
// (2026-10-17). The root fix is to hand this function the whole log; this says so when a
|
|
2634
|
+
// caller cannot.
|
|
2635
|
+
const clipped = /^…\((\d+) earlier characters omitted\)/.exec(log);
|
|
2636
|
+
const header = clipped !== null
|
|
2637
|
+
? `${total} failure${total === 1 ? '' : 's'} in the LAST part of this run’s log — its ` +
|
|
2638
|
+
`first ${clipped[1]} characters were clipped, so a cause that failed earlier is ` +
|
|
2639
|
+
`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' : ''}, ` +
|
|
2640
|
+
`and read the whole run log before concluding:\n`
|
|
2641
|
+
: `${total} failure${total === 1 ? '' : 's'} in this run, grouped by cause — fix every cause ` +
|
|
2642
|
+
`below${connections.length > 0 ? ' the app’s code can produce, and re-send the connection error once as it is' : ' before the next deploy'}:\n`;
|
|
2426
2643
|
// The flaky sentence sits before the ignored one: the ignored sentence is the LAST thing the
|
|
2427
2644
|
// note says, and callers assert on that ending.
|
|
2428
2645
|
const tail = (rerun ? ` Re-run just these once they are fixed: \`${rerun}\`` : '') +
|
|
@@ -2479,10 +2696,21 @@ function failureCauseLine(cause, index) {
|
|
|
2479
2696
|
// twice; the rest are the values the normalised signature replaced with `<n>`.
|
|
2480
2697
|
const body = cause.evidence.filter((line) => cause.message === null || !cause.message.includes(line));
|
|
2481
2698
|
const shortened = body.map((line) => line.length > FAILURE_EVIDENCE_CHARS ? `${line.slice(0, FAILURE_EVIDENCE_CHARS - 1)}…` : line);
|
|
2699
|
+
// The page's own words at the failure (xr-molecule-flash-m122 +23:39): the error line names the
|
|
2700
|
+
// symptom, and these name what the app was saying while it happened. The parenthetical is said
|
|
2701
|
+
// only for the shape the items themselves show — validation copy on a form — where "submitted
|
|
2702
|
+
// before its values loaded" is the reading, and never for arbitrary page text.
|
|
2703
|
+
const showed = cause.pageShowed?.length
|
|
2704
|
+
? `\n the page showed: ${cause.pageShowed.map((t) => `\`${t}\``).join(', ')}` +
|
|
2705
|
+
(cause.pageShowed.some((t) => /\b(?:blank|required|invalid|must be)\b/i.test(t))
|
|
2706
|
+
? ' (the action ran before the form’s values had loaded)'
|
|
2707
|
+
: '')
|
|
2708
|
+
: '';
|
|
2482
2709
|
return (` ${index + 1}. ${cause.kind === 'connection' ? 'CONNECTION error, not the app’s — ' : ''}${signature} — ` +
|
|
2483
2710
|
`${cause.tests.length} test${cause.tests.length === 1 ? '' : 's'} ` +
|
|
2484
2711
|
`in ${files} file${files === 1 ? '' : 's'}: ${tests.join(', ')}${more}` +
|
|
2485
|
-
(shortened.length ? `\n ${shortened.join(' · ')}` : '')
|
|
2712
|
+
(shortened.length ? `\n ${shortened.join(' · ')}` : '') +
|
|
2713
|
+
showed);
|
|
2486
2714
|
}
|
|
2487
2715
|
/**
|
|
2488
2716
|
* The sentence for the causes a recap could not fit: exactly which `file:line` each one was
|
|
@@ -2538,11 +2766,11 @@ function omittedCausesRecap(omitted, run, excluded) {
|
|
|
2538
2766
|
* is not a Playwright run this can narrow.
|
|
2539
2767
|
*
|
|
2540
2768
|
* The `locations` are the paths the run's own log printed, which are relative to the directory
|
|
2541
|
-
* that run started in, so
|
|
2542
|
-
* unless it already changes into one of its own, because the
|
|
2543
|
-
* wherever it starts. The returned command is for AFTER the run
|
|
2544
|
-
* the same app and database, and that is exactly what is
|
|
2545
|
-
* ({@link suiteRunInTheWay}).
|
|
2769
|
+
* that run started in, so the composer resolves them there ({@link scopedRerunCommand}): the
|
|
2770
|
+
* command names that directory unless it already changes into one of its own, because the
|
|
2771
|
+
* executor's next command starts wherever it starts. The returned command is for AFTER the run
|
|
2772
|
+
* ends — it is a second run against the same app and database, and that is exactly what is
|
|
2773
|
+
* refused while the run walks ({@link suiteRunInTheWay}).
|
|
2546
2774
|
*
|
|
2547
2775
|
* @param run - The run in flight: the command it was started with, as it was run, the directory
|
|
2548
2776
|
* that command ran in, and the config it loaded when the caller knows it.
|
|
@@ -2554,7 +2782,10 @@ function omittedCausesRecap(omitted, run, excluded) {
|
|
|
2554
2782
|
export function rerunOfRunningSuite(run, locations, excluded = []) {
|
|
2555
2783
|
if (locations.length === 0)
|
|
2556
2784
|
return null;
|
|
2557
|
-
|
|
2785
|
+
// The run's own directory is the base a relative failure location resolves against: the
|
|
2786
|
+
// reporter prints it from where the runner started, and Playwright matches file arguments
|
|
2787
|
+
// against absolute paths (xr-molecule-flash-m118 +16:34, m124 +18:38).
|
|
2788
|
+
const scoped = scopedRerunCommand(run.command, locations, excluded, run.config, run.cwd);
|
|
2558
2789
|
if (scoped === run.command)
|
|
2559
2790
|
return null;
|
|
2560
2791
|
if (!run.cwd || commandSegments(withoutHeredocs(run.command)).some((s) => s.program === 'cd'))
|
|
@@ -2592,8 +2823,10 @@ export function rerunOfRunningSuite(run, locations, excluded = []) {
|
|
|
2592
2823
|
* @param found - What {@link repeatingFailure} found.
|
|
2593
2824
|
* @param o - The run's facts: its `taskId`, its `log` path, `stop` — the shell command that
|
|
2594
2825
|
* stops it, printed only for an {@link earlyReturnCause} — `universal`, what
|
|
2595
|
-
* {@link universalCause} found (absent means the run is not failing whole),
|
|
2596
|
-
* run was started ({@link rerunOfRunningSuite}) when the caller knows it
|
|
2826
|
+
* {@link universalCause} found (absent means the run is not failing whole), `run`, how the
|
|
2827
|
+
* run was started ({@link rerunOfRunningSuite}) when the caller knows it, and
|
|
2828
|
+
* `editsInvalidateRun`, true when the run measures the sandbox's own app (a local run, where an
|
|
2829
|
+
* edit now changes what the suite is testing and its result stops counting).
|
|
2597
2830
|
* @returns The note.
|
|
2598
2831
|
*/
|
|
2599
2832
|
export function repeatingFailureNote(found, o) {
|
|
@@ -2656,8 +2889,11 @@ export function repeatingFailureNote(found, o) {
|
|
|
2656
2889
|
const files = stop.files.map(shortFile);
|
|
2657
2890
|
const shown = files.slice(0, 4).join(', ');
|
|
2658
2891
|
const moreFiles = files.length > 4 ? `, … and ${files.length - 4} more` : '';
|
|
2892
|
+
const span = files.length === 0
|
|
2893
|
+
? ''
|
|
2894
|
+
: ` — across ${files.length} file${files.length === 1 ? '' : 's'} (${shown}${moreFiles})`;
|
|
2659
2895
|
return (`Every test this run has walked so far has failed the same way — all ${found.count} of ` +
|
|
2660
|
-
`them, ${signature}
|
|
2896
|
+
`them, ${signature}${span}, with nothing ` +
|
|
2661
2897
|
`passing since the first:\n${rows}${more}\n` +
|
|
2662
2898
|
`Every remaining test will fail the same way too, so walking the rest of this run only ` +
|
|
2663
2899
|
`repeats this one cause. Stop it and fix that cause first: \`exec_command: ${o.stop}\`, ` +
|
|
@@ -2674,7 +2910,9 @@ export function repeatingFailureNote(found, o) {
|
|
|
2674
2910
|
if (isCause)
|
|
2675
2911
|
return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
|
|
2676
2912
|
`so one cause is repeating:\n${rows}${more}\n` +
|
|
2677
|
-
|
|
2913
|
+
(o.editsInvalidateRun
|
|
2914
|
+
? `Work out the fix now: apply it when the run ends, not while it walks. Stop it only if every test it walks `
|
|
2915
|
+
: `Start fixing it now, while the run keeps walking. Stop it only if every test it walks `) +
|
|
2678
2916
|
`fails this same way (the app unreachable, a 5xx on every request, the same selector timing ` +
|
|
2679
2917
|
`out on every test of every file so far).${reproduce} ${keepGoing}${thenRun}`);
|
|
2680
2918
|
return (`${found.count} different tests in this run each failed after ~${found.seconds}s — the same ` +
|
|
@@ -2682,7 +2920,10 @@ export function repeatingFailureNote(found, o) {
|
|
|
2682
2920
|
`config sets a timeout spends that timeout on every failure that waits for anything, so ` +
|
|
2683
2921
|
`different missing elements land on the same number. Read these failures before fixing one ` +
|
|
2684
2922
|
`of them:\n${rows}${more}\n` +
|
|
2685
|
-
|
|
2923
|
+
(o.editsInvalidateRun
|
|
2924
|
+
? `Work out the fixes now: apply them when the run ends, not while it walks.`
|
|
2925
|
+
: `Start on them now, while the run keeps walking.`) +
|
|
2926
|
+
`${reproduce} ${keepGoing}${thenRun}`);
|
|
2686
2927
|
}
|
|
2687
2928
|
/**
|
|
2688
2929
|
* Attempt a whitespace-tolerant replacement when an exact `old_string` match
|