@molecule/api-ai-tools 1.9.13 → 1.9.15
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 +28 -7
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +7 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +99 -23
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +157 -16
- package/dist/utilities.d.ts +27 -4
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +103 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-18T11:04:38.283Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -2274,22 +2274,43 @@ into the log twice, which on X1 flash-m83 (2026-10-16) made a `--reporter=list,l
|
|
|
2274
2274
|
_(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/utilities.d.ts`.)_
|
|
2275
2275
|
|
|
2276
2276
|
```typescript
|
|
2277
|
-
function streamingBackgroundCommand(command: string): {
|
|
2277
|
+
function streamingBackgroundCommand(command: string): {
|
|
2278
|
+
command: string
|
|
2279
|
+
changes: string[]
|
|
2280
|
+
error?: string
|
|
2281
|
+
}
|
|
2278
2282
|
```
|
|
2279
2283
|
|
|
2280
2284
|
- `command` — The command as the executor sent it.
|
|
2281
2285
|
|
|
2282
|
-
**Returns:** The command to run and one sentence per change made (empty when none).
|
|
2286
|
+
**Returns:** The command to run and one sentence per change made (empty when none), or `error` when the command starts the suite more than once.
|
|
2283
2287
|
|
|
2284
|
-
#### `
|
|
2288
|
+
#### `suiteRunStatements(command)`
|
|
2285
2289
|
|
|
2286
|
-
|
|
2287
|
-
— rather than a build, an install or a one-off script.
|
|
2290
|
+
Every statement of a command that starts a whole test suite, in command order.
|
|
2288
2291
|
|
|
2289
|
-
|
|
2292
|
+
This is the ONE count of suite runs in a command: {@link isSuiteRunCommand} asks it,
|
|
2293
|
+
{@link streamingBackgroundCommand} refuses a command carrying more than one — each statement
|
|
2294
|
+
starts a whole walk of the same suite, and only the last of them is rewritten — and a re-run
|
|
2295
|
+
composed from a command ({@link ./suite-overlap.js} `scopedRerunCommand`) keeps only the first.
|
|
2296
|
+
|
|
2297
|
+
A statement that invokes a runner only to print is not one of these
|
|
2290
2298
|
({@link commandRunsNoTests}): `npx playwright test --list` prints the tests it would run and
|
|
2291
2299
|
exits, and `npx playwright install`, `vitest list` and `hurl --help` start no test either.
|
|
2292
2300
|
|
|
2301
|
+
```typescript
|
|
2302
|
+
function suiteRunStatements(command: string): string[]
|
|
2303
|
+
```
|
|
2304
|
+
|
|
2305
|
+
- `command` — The shell command.
|
|
2306
|
+
|
|
2307
|
+
**Returns:** One entry per suite-running statement, as the parser read it (empty when none).
|
|
2308
|
+
|
|
2309
|
+
#### `isSuiteRunCommand(command)`
|
|
2310
|
+
|
|
2311
|
+
Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run
|
|
2312
|
+
— rather than a build, an install or a one-off script.
|
|
2313
|
+
|
|
2293
2314
|
```typescript
|
|
2294
2315
|
function isSuiteRunCommand(command: string): boolean
|
|
2295
2316
|
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAKD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAc1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,QAAQ,GAAE,SAAS,MAAM,EAAO,EAChC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAClB,MAAM,
|
|
1
|
+
{"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAKD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAc1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,QAAQ,GAAE,SAAS,MAAM,EAAO,EAChC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAClB,MAAM,CA0CR;AAgHD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,SAAS,EAAE,OAAO,CAAA;IAClB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EACzD,SAAS,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7B,QAAQ,EAAE,SAAS,CAAC,EAAE,GACrB,CAAC,GAAG,IAAI,CAMV;AAqDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQvD"}
|
package/dist/suite-overlap.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
import { posix } from 'node:path';
|
|
32
32
|
import { commandSegments, locatedCommandSegments } from './command-position.js';
|
|
33
33
|
import { commandRunsNoTests } from './no-test-run.js';
|
|
34
|
-
import { shellQuote } from './utilities.js';
|
|
34
|
+
import { shellQuote, suiteRunStatements } from './utilities.js';
|
|
35
35
|
/**
|
|
36
36
|
* A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it
|
|
37
37
|
* WRITES are not what the command itself does.
|
|
@@ -180,7 +180,12 @@ export function scopedRerunCommand(command, locations, excluded = [], config, cw
|
|
|
180
180
|
.replace(/\s*--last-failed\b/g, '')
|
|
181
181
|
.replace(/\s*--retries(?:[= ]\d+)?/g, '')
|
|
182
182
|
.replace(/^(playwright\s+test)\b/, `$1 ${narrowed.join(' ')} --retries=0${carry}${invert}`);
|
|
183
|
-
|
|
183
|
+
// A command that starts the suite AGAIN after this statement is a chain of walks, not a re-run:
|
|
184
|
+
// everything from the next suite runner on is dropped, so the composed command carries ONE walk.
|
|
185
|
+
// X1 xr-molecule-flash-m157 (+14:02) was handed a re-run that still carried the chain's second
|
|
186
|
+
// whole walk — 4.5 minutes measuring code it had not changed.
|
|
187
|
+
const rerunTail = suiteRunStatements(tail).length > 0 ? '' : tail;
|
|
188
|
+
return `${head}${scoped}${rerunTail}`;
|
|
184
189
|
}
|
|
185
190
|
/** A directory a `cd` can name literally: no `~`, no variable, no glob. */
|
|
186
191
|
const LITERAL_DIR = /^[A-Za-z0-9_./+-]+$/;
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAiSD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AA2GD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAqlFxF"}
|
package/dist/tools.js
CHANGED
|
@@ -192,6 +192,58 @@ function logProgress(log) {
|
|
|
192
192
|
const total = Number(last?.[2]);
|
|
193
193
|
return last && total > 0 && done >= 0 && done <= total ? { done, total } : null;
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Playwright's start-of-walk line, `Running 138 tests using 1 worker` — the first thing a walk
|
|
197
|
+
* prints, so a second one in one log is a second walk.
|
|
198
|
+
*/
|
|
199
|
+
const WALK_START = /^[ \t]*Running \d+ tests?\b/gm;
|
|
200
|
+
/**
|
|
201
|
+
* One status line of a runner's closing tally — ` 4 failed`, ` 70 passed (4.5m)`,
|
|
202
|
+
* ` 65 skipped`.
|
|
203
|
+
*/
|
|
204
|
+
const TALLY_STATUS = /^[ \t]*(\d+)[ \t]+(passed|failed|flaky|skipped|interrupted|timed out|did not run)\b[^\n]*/gm;
|
|
205
|
+
/**
|
|
206
|
+
* A runner's CLOSING tally, the line that only prints once a walk has finished: Playwright's
|
|
207
|
+
* duration-bearing `70 passed (4.5m)`, a `1 flaky` summary, or Hurl's `Executed files:` count.
|
|
208
|
+
*
|
|
209
|
+
* The same reading as molecule-dev's `suite-proof.ts` `FINAL_TALLY_RE` (the platform's finished-run
|
|
210
|
+
* detector); this package cannot import it, so the shape is mirrored here and nowhere else.
|
|
211
|
+
*/
|
|
212
|
+
const FINISHED_TALLY = /\b\d+[ \t]+(?:passed|failed)[ \t]+\(\d+(?:\.\d+)?(?:ms|s|m|h)\)|^[ \t]*\d+[ \t]+flaky\b|^[ \t]*Executed files:/m;
|
|
213
|
+
/**
|
|
214
|
+
* Whether a task's log holds a SECOND walk of the same suite — a finished tally followed by a
|
|
215
|
+
* fresh `Running N tests` header — and what the finished walk's tally said.
|
|
216
|
+
*
|
|
217
|
+
* A command that chained two suite runs starts the second walk the moment the first ends, and the
|
|
218
|
+
* wait used to read that log as one slow run ("slow, not stuck") for the whole second walk. X1
|
|
219
|
+
* xr-molecule-flash-m157 (+14:02) waited out 4.5 minutes of a duplicate walk that measured nothing:
|
|
220
|
+
* the first walk had already printed `4 failed / 65 skipped / 70 passed (4.5m)`.
|
|
221
|
+
*
|
|
222
|
+
* @param slice - The log slice this wait read.
|
|
223
|
+
* @returns The finished walk's tally lines, or null when the log shows one walk (or none).
|
|
224
|
+
*/
|
|
225
|
+
function secondSuiteWalk(slice) {
|
|
226
|
+
const header = [...slice.matchAll(WALK_START)].at(-1);
|
|
227
|
+
if (!header || header.index === undefined)
|
|
228
|
+
return null;
|
|
229
|
+
const finished = FINISHED_TALLY.exec(slice);
|
|
230
|
+
// One walk prints its header FIRST and its tally last, so a tally before the last header is
|
|
231
|
+
// another walk's. The slice repeats what it greps and then the log's own tail, so the same
|
|
232
|
+
// tally can appear twice; one line per status, taken at its last position, keeps it once.
|
|
233
|
+
if (!finished || finished.index === undefined || finished.index > header.index)
|
|
234
|
+
return null;
|
|
235
|
+
const seen = new Map();
|
|
236
|
+
for (const match of slice.matchAll(TALLY_STATUS)) {
|
|
237
|
+
if (match.index === undefined || match.index >= header.index)
|
|
238
|
+
continue;
|
|
239
|
+
seen.set(match[2] ?? '', { at: match.index, text: match[0].trim().replace(/\s+/g, ' ') });
|
|
240
|
+
}
|
|
241
|
+
const tally = [...seen.values()]
|
|
242
|
+
.sort((a, b) => a.at - b.at)
|
|
243
|
+
.map((v) => v.text)
|
|
244
|
+
.join(' / ');
|
|
245
|
+
return { tally: tally || 'its closing tally' };
|
|
246
|
+
}
|
|
195
247
|
/**
|
|
196
248
|
* What a wait knows about the command BESIDES "it has not exited": how long it
|
|
197
249
|
* has been running, whether its output is still growing, how far its own log
|
|
@@ -232,11 +284,17 @@ function waitProgressClauses(o) {
|
|
|
232
284
|
? `it has been running at least ${durationText(o.waitedMs)}`
|
|
233
285
|
: `it has been running ${durationText(sinceStart)}`,
|
|
234
286
|
];
|
|
235
|
-
|
|
287
|
+
// A log that already finished one walk and started another says so instead of "slow, not
|
|
288
|
+
// stuck": the growth is the SECOND walk printing, and its counter is not this run's progress.
|
|
289
|
+
const secondWalk = secondSuiteWalk(o.slice);
|
|
290
|
+
if (o.stillWriting && !secondWalk)
|
|
236
291
|
clauses.push(`its output is still growing (${o.bytes} bytes) — slow, not stuck`);
|
|
237
|
-
else if (o.bytes !== null)
|
|
292
|
+
else if (o.bytes !== null && !secondWalk)
|
|
238
293
|
clauses.push(`its log has not grown during this wait (${o.bytes} bytes)`);
|
|
239
|
-
|
|
294
|
+
if (secondWalk)
|
|
295
|
+
clauses.push(`the first walk in this command has finished (${secondWalk.tally}); the command started a ` +
|
|
296
|
+
'SECOND walk of the same suite — stop it (it measures nothing new) and read the first tally');
|
|
297
|
+
const counter = secondWalk ? null : logProgress(o.slice);
|
|
240
298
|
if (counter) {
|
|
241
299
|
clauses.push(`its log is at ${counter.done} of ${counter.total}`);
|
|
242
300
|
// The rate is the task's own elapsed time, never this call's wait: with no
|
|
@@ -2289,6 +2347,11 @@ export function buildTools(backend, config) {
|
|
|
2289
2347
|
// Keep the log growing while it runs (see streamingBackgroundCommand):
|
|
2290
2348
|
// an empty log is what wait_for_task reads as stuck.
|
|
2291
2349
|
const streaming = streamingBackgroundCommand(command);
|
|
2350
|
+
// A command that starts the suite more than once is refused here, before anything is
|
|
2351
|
+
// written or launched: each walk measures the same code, and only the last is rewritten
|
|
2352
|
+
// (X1 xr-molecule-flash-m157 +14:02 — two whole walks chained with `;`).
|
|
2353
|
+
if (streaming.error)
|
|
2354
|
+
return { error: streaming.error };
|
|
2292
2355
|
// …and the trace flag this tool added (above), which the streaming rewrite never saw:
|
|
2293
2356
|
// its changes are what this branch reports the run's own settings in.
|
|
2294
2357
|
if (traced.change)
|
|
@@ -2859,6 +2922,14 @@ export function buildTools(backend, config) {
|
|
|
2859
2922
|
// instruction — and not on a finished run's result, where `failureCausesNote` reads the
|
|
2860
2923
|
// whole log instead of the 40 KB tail a mid-run look is given.
|
|
2861
2924
|
const midRun = midRunNote ? ` ${midRunNote}` : '';
|
|
2925
|
+
// A log that already finished one walk and started a second is not a run to keep waiting
|
|
2926
|
+
// on: the first walk's tally is the measurement and the second only repeats it, so the
|
|
2927
|
+
// advice below hands over the stop instead of "keep waiting" (m157 +14:02).
|
|
2928
|
+
const secondWalk = secondSuiteWalk(soFar) !== null;
|
|
2929
|
+
const stopInstead = secondWalk
|
|
2930
|
+
? ` Stop it (\`exec_command: ${await taskStopCommand(backend, taskId)}\`) and read the ` +
|
|
2931
|
+
`first walk's tally in ${log} — the second walk measures nothing new.`
|
|
2932
|
+
: null;
|
|
2862
2933
|
if (stillWriting && waitedBefore + waitedMs >= stuckAfterMs()) {
|
|
2863
2934
|
waitedByTask.set(taskId, 0);
|
|
2864
2935
|
return {
|
|
@@ -2866,8 +2937,11 @@ export function buildTools(backend, config) {
|
|
|
2866
2937
|
status: 'running',
|
|
2867
2938
|
waitedMs,
|
|
2868
2939
|
...sliceField,
|
|
2869
|
-
note: `${progress}
|
|
2870
|
-
|
|
2940
|
+
note: `${progress}` +
|
|
2941
|
+
(stopInstead ??
|
|
2942
|
+
` Call wait_for_task again to keep waiting — it returns the moment the ` +
|
|
2943
|
+
`command ends and reports the failures as they appear.`) +
|
|
2944
|
+
`${midRun}`,
|
|
2871
2945
|
};
|
|
2872
2946
|
}
|
|
2873
2947
|
if (waitedBefore + waitedMs >= stuckAfterMs()) {
|
|
@@ -2889,24 +2963,26 @@ export function buildTools(backend, config) {
|
|
|
2889
2963
|
// No "Still running after Ns" lead here either: the clauses already say
|
|
2890
2964
|
// how long it has been running, and the lead said it a second time in
|
|
2891
2965
|
// the other format (X1 flash-m87 §+47:20).
|
|
2892
|
-
note: `${progress}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2966
|
+
note: `${progress}` +
|
|
2967
|
+
(stopInstead ??
|
|
2968
|
+
` Call wait_for_task again to ` +
|
|
2969
|
+
`keep waiting — its result carries the log output. ` +
|
|
2970
|
+
(soFar.trim()
|
|
2971
|
+
? `outputSoFar is the failure lines so far (up to ${RUNNING_FAILURE_LINES}) and then the end of ` +
|
|
2972
|
+
`the log, not the whole run — \`grep -n '✘' ${log} | head -40\` lists every failure, ` +
|
|
2973
|
+
`\`grep -n -A 20 '<test name>' ${log}\` reads one failure's error: this run writes ` +
|
|
2974
|
+
`each failure's own report into the log as that test fails, right under its ✘ line, ` +
|
|
2975
|
+
`not only in the summary at the end. `
|
|
2976
|
+
: `Nothing has been printed yet — \`grep -n '✘' ${log} | head -40\` lists failures as they appear. `) +
|
|
2977
|
+
// What an edit does to this run depends on what the run MEASURES: the sandbox's own
|
|
2978
|
+
// app is re-served from app/src as it is edited, so the run's result stops counting; a
|
|
2979
|
+
// run against the deployed build is untouched by a local edit, and the executor can
|
|
2980
|
+
// spend the rest of the run fixing (xr-molecule-flash-m122 +19:00: a deployed run was
|
|
2981
|
+
// told editing changes what it tests, then handed "Start fixing it now" in the same
|
|
2982
|
+
// result — it waited out 1 min 11 s of free fixing time).
|
|
2983
|
+
(started !== undefined && suiteCommandTarget(started.command) !== 'local'
|
|
2984
|
+
? `Meanwhile fix what it has already named: this run tests the deployed build, so editing app/src or api/src now does not change what it tests — fix the causes it has already named while it walks, run nothing against the app until it ends, then deploy once.`
|
|
2985
|
+
: `Meanwhile do work that leaves the code under test alone (read the failures so far, plan the fixes): editing app/src or api/src now changes the code the run is testing, and its result no longer counts.`)) +
|
|
2910
2986
|
`${midRun}`,
|
|
2911
2987
|
};
|
|
2912
2988
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,6FAA6F;IAC7F,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B;;;;OAIG;IACH,OAAO,EAAE,WAAW,EAAE,CAAA;CACvB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,6FAA6F;IAC7F,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B;;;;OAIG;IACH,OAAO,EAAE,WAAW,EAAE,CAAA;CACvB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAUxC,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,iDAAiD;AACjD,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAO,MA+iBvC,CAAA;AAoDD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAkCnE;AA2KD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAKnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAG5D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAGhE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAMzE;AAkDD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IACrC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AA+dD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,SAAS,MAAM,EAAO,EACvC,WAAW,GAAE,SAAS,MAAM,EAAO,EACnC,MAAM,GAAE,cAAmB,GAC1B,MAAM,GAAG,IAAI,CAsTf;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,8BAA8B,CAAA;AAwG/D,oGAAoG;AACpG,MAAM,WAAW,qBAAqB;IACpC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5C,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA2BjC"}
|
package/dist/trace-artifacts.js
CHANGED
|
@@ -42,6 +42,13 @@ export const MAX_TRACED_CONSOLE = 4;
|
|
|
42
42
|
export const MAX_TRACED_ACTIONS = 40;
|
|
43
43
|
/** How much of a 4xx request's sent body (or its response body) the trace read keeps. */
|
|
44
44
|
export const MAX_TRACED_BODY_CHARS = 300;
|
|
45
|
+
/**
|
|
46
|
+
* How much of a websocket's frames the trace read keeps. Longer than {@link MAX_TRACED_BODY_CHARS}
|
|
47
|
+
* because a frame read back to its own message still has the message, the module id and the
|
|
48
|
+
* location to carry — Vite's transform error alone is a sentence longer than the plain body cap,
|
|
49
|
+
* and the note prints its first 120–200 characters anyway (xr-molecule-flash-m157).
|
|
50
|
+
*/
|
|
51
|
+
const MAX_TRACED_WEBSOCKET_CHARS = 600;
|
|
45
52
|
/** How many tests the note in a run's result names. */
|
|
46
53
|
export const MAX_TRACE_NOTE_TESTS = 5;
|
|
47
54
|
/** How many requests the note names per test. */
|
|
@@ -97,6 +104,7 @@ const MAX_NOTE = ${MAX_TRACE_NOTE_REQUESTS}
|
|
|
97
104
|
const MAX_CONSOLE = ${MAX_TRACED_CONSOLE}
|
|
98
105
|
const MAX_ACTIONS = ${MAX_TRACED_ACTIONS}
|
|
99
106
|
const MAX_BODY = ${MAX_TRACED_BODY_CHARS}
|
|
107
|
+
const MAX_WS = ${MAX_TRACED_WEBSOCKET_CHARS}
|
|
100
108
|
const MAX_URL = 200
|
|
101
109
|
// A console line that is not an error but still has to reach the note (m148): one a molecule
|
|
102
110
|
// package wrote — it names the package, the \`[molecule/…]\` prefix, or the \`[scope]\` prefix
|
|
@@ -199,6 +207,51 @@ function bodyText(buf, entries, value, max) {
|
|
|
199
207
|
return text.indexOf('\\u0000') === -1 ? text : ''
|
|
200
208
|
}
|
|
201
209
|
|
|
210
|
+
// A websocket's content is a JSONL stream of frames (\`{type, time, opcode, data}\`), and the data
|
|
211
|
+
// is the page's own message. The envelope is ~35 characters on every frame, so cutting the raw
|
|
212
|
+
// stream lands inside it and never reaches the text that names a cause: xr-molecule-flash-m157's
|
|
213
|
+
// Vite HMR error frame was \`{"type":"receive","time":…,"opcode":1,"data":"{\\"type\\":\\"error\\",
|
|
214
|
+
// \\"err\\":{\\"message\\":\\"Cannot import non-asset file /styles.css which is inside /public …\` and
|
|
215
|
+
// every note stopped at \`{"type":"error","err":{"message":"Cannot…\`. Read the frames and print
|
|
216
|
+
// what they carry — a Vite error's message, id and loc, or the JSON a frame sends — so the cut
|
|
217
|
+
// lands in the message itself.
|
|
218
|
+
function websocketText(raw) {
|
|
219
|
+
const errors = []
|
|
220
|
+
const others = []
|
|
221
|
+
for (const line of String(raw).split('\\n')) {
|
|
222
|
+
if (!line.trim()) continue
|
|
223
|
+
let frame
|
|
224
|
+
try { frame = JSON.parse(line) } catch (e) { continue }
|
|
225
|
+
if (!frame || typeof frame.data !== 'string') continue
|
|
226
|
+
let data = frame.data
|
|
227
|
+
try { data = JSON.parse(frame.data) } catch (e) { /* a text frame: print the data as it is */ }
|
|
228
|
+
const err = data && typeof data === 'object' && data.type === 'error' ? data.err : null
|
|
229
|
+
if (err && typeof err.message === 'string') {
|
|
230
|
+
let text = 'vite error: ' + err.message
|
|
231
|
+
if (err.id !== undefined && err.id !== null && err.id !== '')
|
|
232
|
+
text += ' (id: ' + String(err.id) + ')'
|
|
233
|
+
const loc = err.loc
|
|
234
|
+
if (loc && typeof loc === 'object') {
|
|
235
|
+
const file = typeof loc.file === 'string' && loc.file ? loc.file : String(err.id || '')
|
|
236
|
+
text += ' (loc: ' + file +
|
|
237
|
+
(typeof loc.line === 'number' ? ':' + loc.line + ':' + (typeof loc.column === 'number' ? loc.column : 0) : '') +
|
|
238
|
+
')'
|
|
239
|
+
} else if (typeof loc === 'string' && loc) text += ' (loc: ' + loc + ')'
|
|
240
|
+
errors.push(text)
|
|
241
|
+
} else if (typeof data === 'string') others.push(data)
|
|
242
|
+
else others.push(JSON.stringify(data))
|
|
243
|
+
}
|
|
244
|
+
return errors.concat(others).join(' ')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// What a request got back, as the note reads it: a websocket's frames are read as the messages
|
|
248
|
+
// they carry (m157), every other request's body as stored.
|
|
249
|
+
function gotText(buf, entries, r) {
|
|
250
|
+
if (r.type !== 'websocket') return bodyText(buf, entries, r.content, MAX_BODY)
|
|
251
|
+
const raw = bodyText(buf, entries, r.content, 8000)
|
|
252
|
+
return (websocketText(raw) || raw).slice(0, MAX_WS)
|
|
253
|
+
}
|
|
254
|
+
|
|
202
255
|
let origin = ''
|
|
203
256
|
function pathOf(url) {
|
|
204
257
|
try {
|
|
@@ -431,7 +484,7 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
431
484
|
for (const i of actionIndexes) {
|
|
432
485
|
const r = requests[i]
|
|
433
486
|
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
434
|
-
const got =
|
|
487
|
+
const got = gotText(buf, entries, r)
|
|
435
488
|
if (sent) r.sent = sent
|
|
436
489
|
if (got) r.got = got
|
|
437
490
|
}
|
|
@@ -439,7 +492,7 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
439
492
|
if (r.status < 400 || r.status >= 500) continue
|
|
440
493
|
if (/^https?:/i.test(r.path)) continue
|
|
441
494
|
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
442
|
-
const got =
|
|
495
|
+
const got = gotText(buf, entries, r)
|
|
443
496
|
if (sent) r.sent = sent
|
|
444
497
|
if (got) r.got = got
|
|
445
498
|
}
|
|
@@ -509,7 +562,7 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
509
562
|
if (leadIndex !== -1) {
|
|
510
563
|
const r = requests[leadIndex]
|
|
511
564
|
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
512
|
-
const got =
|
|
565
|
+
const got = gotText(buf, entries, r)
|
|
513
566
|
lead = wire(r)
|
|
514
567
|
if (sent) lead.sent = sent
|
|
515
568
|
if (got) lead.got = got
|
|
@@ -1178,19 +1231,36 @@ function sameOriginPath(request) {
|
|
|
1178
1231
|
* the request carried no `Authorization` header at all and the app's hand-rolled session store
|
|
1179
1232
|
* never handed its token to the shared client.
|
|
1180
1233
|
*
|
|
1234
|
+
* xr-molecule-flash-m156 (+16:15): the same 401 shape after a reload, and the page's own console
|
|
1235
|
+
* named the cause — `@molecule/app-http` logged `no client is bonded`. The auth setup ran before
|
|
1236
|
+
* the HTTP setup and kept the fallback client, so the restored token landed on a client nothing
|
|
1237
|
+
* sent through. When the trace carries that line, the no-header sentence names the ordering fix
|
|
1238
|
+
* instead of leaving the run to re-derive it.
|
|
1239
|
+
*
|
|
1181
1240
|
* The trace cannot name the file that made the call: a real HAR entry carries the request, response,
|
|
1182
1241
|
* `_resourceType`, `_monotonicTime` and a frame ref, but no initiator stack. So the sentence names
|
|
1183
1242
|
* the shared client and the auth bond instead of guessing a file.
|
|
1184
1243
|
*
|
|
1185
1244
|
* @param request - The refused request, a 401 or 403 on the app's own origin.
|
|
1245
|
+
* @param unbondedClient - Whether the traced page logged `no client is bonded`, which explains a
|
|
1246
|
+
* request that carried no `Authorization` header.
|
|
1186
1247
|
* @returns The sentence the note appends.
|
|
1187
1248
|
*/
|
|
1188
|
-
function authRefusalSentence(request) {
|
|
1249
|
+
function authRefusalSentence(request, unbondedClient = false) {
|
|
1189
1250
|
const label = requestLabel(request);
|
|
1190
1251
|
// The header's scheme, when it parsed; presence alone is still the server's half of the wire.
|
|
1191
1252
|
const carried = request.authorization !== undefined
|
|
1192
1253
|
? `with \`Authorization: ${request.authorization} …\``
|
|
1193
1254
|
: 'with an `Authorization` header';
|
|
1255
|
+
// The console line names the mechanism the no-header branch otherwise has to guess at:
|
|
1256
|
+
// `getClient()` ran before `setupHttpDefault()` and the token was mirrored into the
|
|
1257
|
+
// client that later setup replaced (xr-molecule-flash-m156).
|
|
1258
|
+
const unbonded = unbondedClient
|
|
1259
|
+
? ' The page logged `no client is bonded` — something called `getClient()` from ' +
|
|
1260
|
+
'`@molecule/app-http` before `setupHttpDefault()` ran and kept that fallback client; ' +
|
|
1261
|
+
'in `app/src/bonds/index.ts` call `setupHttpDefault()` before `setupAuthDefault()`, ' +
|
|
1262
|
+
'and read the client per request instead of capturing it at setup.'
|
|
1263
|
+
: '';
|
|
1194
1264
|
if (request.status === 403) {
|
|
1195
1265
|
return request.sentAuthorization === true
|
|
1196
1266
|
? `The browser sent \`${label}\` ${carried} and the API answered 403 — the token was ` +
|
|
@@ -1200,7 +1270,8 @@ function authRefusalSentence(request) {
|
|
|
1200
1270
|
'`Authorization` header, so either the client did not attach the token (check that the ' +
|
|
1201
1271
|
'request goes through the app’s HTTP client that the auth bond configures, and that the ' +
|
|
1202
1272
|
'token is stored where the client reads it) or the route refuses this user: read the ' +
|
|
1203
|
-
'route’s role/permission check in your own source.'
|
|
1273
|
+
'route’s role/permission check in your own source.' +
|
|
1274
|
+
unbonded;
|
|
1204
1275
|
}
|
|
1205
1276
|
if (request.sentAuthorization === true) {
|
|
1206
1277
|
return (`The browser sent \`${label}\` ${carried} and the API refused it: the token was sent and ` +
|
|
@@ -1218,7 +1289,8 @@ function authRefusalSentence(request) {
|
|
|
1218
1289
|
? 'The request did carry a `Cookie` header, so if this app signs in with a session cookie ' +
|
|
1219
1290
|
'that is the credential the server did not accept. '
|
|
1220
1291
|
: '') +
|
|
1221
|
-
'The API handler is not the problem.'
|
|
1292
|
+
'The API handler is not the problem.' +
|
|
1293
|
+
unbonded);
|
|
1222
1294
|
}
|
|
1223
1295
|
/**
|
|
1224
1296
|
* The note's credential-refusal advice, one sentence per distinct shape — at most two.
|
|
@@ -1234,23 +1306,67 @@ function authRefusalSentence(request) {
|
|
|
1234
1306
|
function authRefusalAdvice(items) {
|
|
1235
1307
|
const sentences = [];
|
|
1236
1308
|
const seen = new Set();
|
|
1309
|
+
// A page that logged `no client is bonded` explains every request it sent without a
|
|
1310
|
+
// token, so the advice names that mechanism once for the run
|
|
1311
|
+
// (xr-molecule-flash-m156).
|
|
1312
|
+
const unbondedClient = items.some((item) => item.console.some((line) => line.includes('no client is bonded')));
|
|
1237
1313
|
for (const item of items) {
|
|
1238
1314
|
for (const request of item.requests) {
|
|
1239
1315
|
if (request.status !== 401 && request.status !== 403)
|
|
1240
1316
|
continue;
|
|
1241
1317
|
if (sameOriginPath(request) === null)
|
|
1242
1318
|
continue;
|
|
1319
|
+
// A 4xx on the logout endpoint is the shape the row itself answers: the auth client
|
|
1320
|
+
// ignores it and clears the session, and the page owes the navigation (m156).
|
|
1321
|
+
if (isLogoutRequest(request))
|
|
1322
|
+
continue;
|
|
1243
1323
|
const key = `${request.status} ${request.sentAuthorization ? (request.authorization ?? 'header') : 'none'} ${request.sentCookie ? 'cookie' : ''}`;
|
|
1244
1324
|
if (seen.has(key))
|
|
1245
1325
|
continue;
|
|
1246
1326
|
seen.add(key);
|
|
1247
|
-
sentences.push(authRefusalSentence(request));
|
|
1327
|
+
sentences.push(authRefusalSentence(request, unbondedClient));
|
|
1248
1328
|
if (sentences.length === 2)
|
|
1249
1329
|
return ` ${sentences.join(' ')}`;
|
|
1250
1330
|
}
|
|
1251
1331
|
}
|
|
1252
1332
|
return sentences.length === 0 ? '' : ` ${sentences.join(' ')}`;
|
|
1253
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Whether a request went to an auth client's logout endpoint. The client's default
|
|
1336
|
+
* `logoutEndpoint` is `/users/logout` (`@molecule/app-auth`), and a project may name its own
|
|
1337
|
+
* (`/auth/logout`), so the last segment is what identifies it — never a literal path.
|
|
1338
|
+
*
|
|
1339
|
+
* @param request - One request a trace recorded.
|
|
1340
|
+
* @returns True for a POST to a path ending in `/logout`.
|
|
1341
|
+
*/
|
|
1342
|
+
function isLogoutRequest(request) {
|
|
1343
|
+
if (request.method !== 'POST')
|
|
1344
|
+
return false;
|
|
1345
|
+
return /\/(?:users\/)?logout$/i.test(requestPath(request));
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* The sentence a 4xx on the auth client's logout endpoint owes when the page then stayed put.
|
|
1349
|
+
*
|
|
1350
|
+
* `@molecule/app-auth`'s `logout()` calls the endpoint inside a `.catch` and clears the session in
|
|
1351
|
+
* `finally` (`packages/app/core/auth/src/client.ts`): a 401 there is EXPECTED on a sign-out and is
|
|
1352
|
+
* not the failure. What the test waits for is the URL moving, which the page has to do itself.
|
|
1353
|
+
* xr-molecule-flash-m156 (+24:02 → +33:52) read `POST /api/users/logout → 401` as a broken
|
|
1354
|
+
* endpoint, added a backend logout route, and paid a re-run and a redeploy — while the real cause
|
|
1355
|
+
* was the `navigate('/')` the refactor had dropped.
|
|
1356
|
+
*
|
|
1357
|
+
* @param request - The 4xx logout request.
|
|
1358
|
+
* @param pageUrl - The URL the page stayed at, when the trace recorded one.
|
|
1359
|
+
* @returns The sentence the note appends to the row.
|
|
1360
|
+
*/
|
|
1361
|
+
function logoutIgnoredSentence(request, pageUrl) {
|
|
1362
|
+
const where = pageUrl === undefined ? 'The page' : `\`${pageUrl}\``;
|
|
1363
|
+
return (`The ${request.status} on \`${requestLabel(request)}\` is ignored by the auth client — ` +
|
|
1364
|
+
'`logout()` catches that call and clears the session in `finally` (`@molecule/app-auth`), so ' +
|
|
1365
|
+
'the request is not what broke the test and the API is not the fix; do not add or change a ' +
|
|
1366
|
+
`logout route. ${where} stayed on the same URL: the page has to navigate itself after ` +
|
|
1367
|
+
"`logout()` (call `navigate('/')`, or the route the suite waits for) in the handler that " +
|
|
1368
|
+
'signs out.');
|
|
1369
|
+
}
|
|
1254
1370
|
/**
|
|
1255
1371
|
* The API path prefixes the traced requests themselves show.
|
|
1256
1372
|
*
|
|
@@ -1452,8 +1568,15 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1452
1568
|
: '';
|
|
1453
1569
|
// A credential refusal (401/403 on the app's own API) is not the handler's answer: the trace's
|
|
1454
1570
|
// own `Authorization` record says whether the CLIENT attached the token at all (m148 +29:18).
|
|
1455
|
-
const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null &&
|
|
1571
|
+
const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null &&
|
|
1572
|
+
(request.status === 401 || request.status === 403) &&
|
|
1573
|
+
// A 4xx on the logout endpoint is not a credential refusal to diagnose: the auth client
|
|
1574
|
+
// ignores it and clears the session anyway, and the row says so itself (m156).
|
|
1575
|
+
!isLogoutRequest(request));
|
|
1456
1576
|
const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
|
|
1577
|
+
// Every recorded failure is the logout endpoint answering 4xx: the row's own sentence explains
|
|
1578
|
+
// it, and neither the handler lead nor the credential reading applies (m156 +24:02).
|
|
1579
|
+
const onlyLogoutFailures = failedRequests.length > 0 && failedRequests.every((request) => isLogoutRequest(request));
|
|
1457
1580
|
const authAdvice = authRefusalAdvice(traced);
|
|
1458
1581
|
// The stalled-navigation reading, per traced test: non-null only when the RUNNER says this
|
|
1459
1582
|
// test's failing operation is a navigation and a document subresource still pending at the end
|
|
@@ -1524,6 +1647,19 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1524
1647
|
? `the page then navigated to ${step.pageAfter}`
|
|
1525
1648
|
: `the page stayed at ${step.pageAfter}`);
|
|
1526
1649
|
}
|
|
1650
|
+
// A 4xx on the logout endpoint with the page unmoved is not a backend failure: the auth
|
|
1651
|
+
// client swallows the call and clears the session, and the URL can only move if the page
|
|
1652
|
+
// navigates itself (xr-molecule-flash-m156 +24:02: the refactor dropped `navigate('/')` and
|
|
1653
|
+
// the run went and added a logout route instead).
|
|
1654
|
+
const stepLast = stepRequests.length > 0 ? stepRequests[stepRequests.length - 1] : undefined;
|
|
1655
|
+
if (stepLast !== undefined &&
|
|
1656
|
+
stepLast.status >= 400 &&
|
|
1657
|
+
stepLast.status < 500 &&
|
|
1658
|
+
isLogoutRequest(stepLast) &&
|
|
1659
|
+
step.pageBefore !== undefined &&
|
|
1660
|
+
step.pageAfter !== undefined &&
|
|
1661
|
+
step.pageBefore === step.pageAfter)
|
|
1662
|
+
parts.push(logoutIgnoredSentence(stepLast, step.pageAfter));
|
|
1527
1663
|
}
|
|
1528
1664
|
else if (lead !== undefined) {
|
|
1529
1665
|
parts.push(leadRequestLine(lead));
|
|
@@ -1626,14 +1762,19 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1626
1762
|
(anyFailed
|
|
1627
1763
|
? everyFailureLacksPrefix
|
|
1628
1764
|
? ''
|
|
1629
|
-
:
|
|
1630
|
-
? //
|
|
1631
|
-
//
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1765
|
+
: onlyLogoutFailures
|
|
1766
|
+
? // The rows already say it: a 4xx on the logout endpoint is ignored by the auth client
|
|
1767
|
+
// and the page's own navigation is the missing step. The handler lead would send the
|
|
1768
|
+
// run at the backend (m156 +33:52 added a logout route).
|
|
1769
|
+
''
|
|
1770
|
+
: everyFailureAuth
|
|
1771
|
+
? // Every failure was a credential refusal: the handler is not the problem, and the
|
|
1772
|
+
// auth advice already names what is (m148 was sent to the handler six walks running).
|
|
1773
|
+
authAdvice
|
|
1774
|
+
: 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
|
|
1775
|
+
'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
|
|
1776
|
+
'user action silently — log it.' +
|
|
1777
|
+
authAdvice
|
|
1637
1778
|
: anyStalled
|
|
1638
1779
|
? report.productionBuildAdvised
|
|
1639
1780
|
? 'No listed request failed — none got a 4xx/5xx or a network error — but a navigation ' +
|
package/dist/utilities.d.ts
CHANGED
|
@@ -174,22 +174,45 @@ export declare const PLAYWRIGHT_FAILURES_INLINE_ENV = "PLAYWRIGHT_LIST_PRINT_FAI
|
|
|
174
174
|
* something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
175
175
|
* neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}).
|
|
176
176
|
*
|
|
177
|
+
* A command that starts the suite MORE THAN ONCE is not launched at all: every run it carries is
|
|
178
|
+
* a whole walk of the same suite against the same app, and only the last step is rewritten, so the
|
|
179
|
+
* earlier walks run unmeasured for nothing. X1 xr-molecule-flash-m157 (+14:02) chained two whole
|
|
180
|
+
* walks of one 138-test suite with `;` — 4.5 minutes each — and the second measured nothing the
|
|
181
|
+
* first had not. The platform already refuses a second CONCURRENT run against one app; this is the
|
|
182
|
+
* same hazard inside one command ({@link suiteRunStatements}). `error` then carries the refusal,
|
|
183
|
+
* naming each suite step and the ONE command to send instead (the last suite step, rewritten as
|
|
184
|
+
* this function rewrites a run), and nothing is launched.
|
|
185
|
+
*
|
|
177
186
|
* @param command - The command as the executor sent it.
|
|
178
|
-
* @returns The command to run and one sentence per change made (empty when none)
|
|
187
|
+
* @returns The command to run and one sentence per change made (empty when none), or `error` when
|
|
188
|
+
* the command starts the suite more than once.
|
|
179
189
|
*/
|
|
180
190
|
export declare function streamingBackgroundCommand(command: string): {
|
|
181
191
|
command: string;
|
|
182
192
|
changes: string[];
|
|
193
|
+
error?: string;
|
|
183
194
|
};
|
|
184
195
|
/**
|
|
185
|
-
*
|
|
186
|
-
* — rather than a build, an install or a one-off script.
|
|
196
|
+
* Every statement of a command that starts a whole test suite, in command order.
|
|
187
197
|
*
|
|
188
|
-
*
|
|
198
|
+
* This is the ONE count of suite runs in a command: {@link isSuiteRunCommand} asks it,
|
|
199
|
+
* {@link streamingBackgroundCommand} refuses a command carrying more than one — each statement
|
|
200
|
+
* starts a whole walk of the same suite, and only the last of them is rewritten — and a re-run
|
|
201
|
+
* composed from a command ({@link ./suite-overlap.js} `scopedRerunCommand`) keeps only the first.
|
|
202
|
+
*
|
|
203
|
+
* A statement that invokes a runner only to print is not one of these
|
|
189
204
|
* ({@link commandRunsNoTests}): `npx playwright test --list` prints the tests it would run and
|
|
190
205
|
* exits, and `npx playwright install`, `vitest list` and `hurl --help` start no test either.
|
|
191
206
|
*
|
|
192
207
|
* @param command - The shell command.
|
|
208
|
+
* @returns One entry per suite-running statement, as the parser read it (empty when none).
|
|
209
|
+
*/
|
|
210
|
+
export declare function suiteRunStatements(command: string): string[];
|
|
211
|
+
/**
|
|
212
|
+
* Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run
|
|
213
|
+
* — rather than a build, an install or a one-off script.
|
|
214
|
+
*
|
|
215
|
+
* @param command - The shell command.
|
|
193
216
|
* @returns True when the command starts one of those runners.
|
|
194
217
|
*/
|
|
195
218
|
export declare function isSuiteRunCommand(command: string): boolean;
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAW3E;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAEE,CAAA;AAuGhD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD;AAKD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAsCzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAUpF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQnE;AAuDD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,0CAA0C,CAAA;AAErF
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
|
package/dist/utilities.js
CHANGED
|
@@ -388,8 +388,18 @@ export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_IN
|
|
|
388
388
|
* something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
389
389
|
* neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}).
|
|
390
390
|
*
|
|
391
|
+
* A command that starts the suite MORE THAN ONCE is not launched at all: every run it carries is
|
|
392
|
+
* a whole walk of the same suite against the same app, and only the last step is rewritten, so the
|
|
393
|
+
* earlier walks run unmeasured for nothing. X1 xr-molecule-flash-m157 (+14:02) chained two whole
|
|
394
|
+
* walks of one 138-test suite with `;` — 4.5 minutes each — and the second measured nothing the
|
|
395
|
+
* first had not. The platform already refuses a second CONCURRENT run against one app; this is the
|
|
396
|
+
* same hazard inside one command ({@link suiteRunStatements}). `error` then carries the refusal,
|
|
397
|
+
* naming each suite step and the ONE command to send instead (the last suite step, rewritten as
|
|
398
|
+
* this function rewrites a run), and nothing is launched.
|
|
399
|
+
*
|
|
391
400
|
* @param command - The command as the executor sent it.
|
|
392
|
-
* @returns The command to run and one sentence per change made (empty when none)
|
|
401
|
+
* @returns The command to run and one sentence per change made (empty when none), or `error` when
|
|
402
|
+
* the command starts the suite more than once.
|
|
393
403
|
*/
|
|
394
404
|
export function streamingBackgroundCommand(command) {
|
|
395
405
|
// A command that runs no test is not a run to keep a growing log for: nothing about it is
|
|
@@ -442,6 +452,30 @@ export function streamingBackgroundCommand(command) {
|
|
|
442
452
|
// options are parsed and placed against this text, while the closer is kept in the command.
|
|
443
453
|
const lastStep = rawLastStep.replace(/[\s)}]+$/, '');
|
|
444
454
|
const stepStart = rawLastStep === '' ? out.length : Math.max(0, out.lastIndexOf(rawLastStep));
|
|
455
|
+
// A command that starts the suite more than once is not launched: each statement is a whole walk
|
|
456
|
+
// of the same suite against the same app, only the last of them is rewritten, and the earlier
|
|
457
|
+
// ones measure nothing the first did. X1 xr-molecule-flash-m157 (+14:02) chained two whole walks
|
|
458
|
+
// of one 138-test suite with `;` (4.5 minutes each); the platform already refuses a second
|
|
459
|
+
// CONCURRENT run against one app, and this is the same hazard inside one command.
|
|
460
|
+
const suiteSteps = suiteRunStatements(out);
|
|
461
|
+
if (suiteSteps.length > 1) {
|
|
462
|
+
// The ONE run to send instead: the last step that runs the suite, rewritten exactly as this
|
|
463
|
+
// function rewrites a run (its own count is one — guarded, so this cannot recurse).
|
|
464
|
+
const lastSuitePart = [...parts]
|
|
465
|
+
.reverse()
|
|
466
|
+
.map((p) => p.trim())
|
|
467
|
+
.find((p) => isSuiteRunCommand(p));
|
|
468
|
+
const single = lastSuitePart && suiteRunStatements(lastSuitePart).length === 1
|
|
469
|
+
? streamingBackgroundCommand(lastSuitePart).command
|
|
470
|
+
: (lastSuitePart ?? '');
|
|
471
|
+
return {
|
|
472
|
+
command: out,
|
|
473
|
+
changes: [],
|
|
474
|
+
error: `This command runs the suite ${suiteSteps.length === 2 ? 'twice' : `${suiteSteps.length} times`} ` +
|
|
475
|
+
`(${suiteSteps.map((s, i) => `step ${i + 1}: \`${s}\``).join(', ')}). One walk is the ` +
|
|
476
|
+
`measurement; run exactly one: \`${single}\``,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
445
479
|
// The last step runs Playwright — whether or not anything is piped from it. Keying this on
|
|
446
480
|
// "nothing is piped out of the last step" withheld the reporter, the inline-failure switch and
|
|
447
481
|
// `--retries=0` from exactly the runs whose output was filtered (xr-molecule-flash-m101
|
|
@@ -478,12 +512,18 @@ export function streamingBackgroundCommand(command) {
|
|
|
478
512
|
};
|
|
479
513
|
// A diagnosis or a re-run (capped, last-failed, grepped, or named files) walks each failure
|
|
480
514
|
// once: the suite's own `retries` doubled every failure of X1 flash-m71's first whole run,
|
|
481
|
-
// which had dropped the `--retries=0` it used one command earlier.
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
515
|
+
// which had dropped the `--retries=0` it used one command earlier. A whole run whose TARGET is
|
|
516
|
+
// local — its env names `localhost`/`127.0.0.1`/`0.0.0.0`, or names no URL at all, as the
|
|
517
|
+
// wrapper config's default does — is not the proof run either and walks them once too:
|
|
518
|
+
// xr-molecule-flash-m155 (+11:34) spent 10 minutes on its first local whole run, and the
|
|
519
|
+
// suite's `retries: 1` walked each of its 68 failures a second time (0 flaky) for nothing.
|
|
520
|
+
// The suite's retries are kept only for a run that names a non-local URL — the deployed proof
|
|
521
|
+
// (xr-molecule-flash-m89), where they absorb a network blip. Which runs are diagnoses is
|
|
522
|
+
// {@link isDiagnosisRun}'s answer and nobody else's: the platform's notes ask the same
|
|
523
|
+
// function, so a run cannot be a diagnosis to one of them and the whole suite to the other.
|
|
524
|
+
// Which targets are local is {@link suiteCommandTarget}'s answer.
|
|
486
525
|
const diagnosis = isDiagnosisRun(lastStep);
|
|
526
|
+
const localTarget = suiteCommandTarget(lastStep) === 'local';
|
|
487
527
|
// The last step's reporters: one printer, and it is `list`. Every branch below only ever moves
|
|
488
528
|
// the command TO that shape — a reporter set is never left with two printers in it.
|
|
489
529
|
const reporters = finalPlaywrightRun ? lastReporterFlag(lastStep) : null;
|
|
@@ -549,9 +589,9 @@ export function streamingBackgroundCommand(command) {
|
|
|
549
589
|
'summary at the end — so a run that is still walking already says what its failures are, ' +
|
|
550
590
|
'and you need not wait for it to end to start on them.');
|
|
551
591
|
}
|
|
552
|
-
if (finalPlaywrightRun && diagnosis && !/--retries\b/.test(lastStep)) {
|
|
592
|
+
if (finalPlaywrightRun && (diagnosis || localTarget) && !/--retries\b/.test(lastStep)) {
|
|
553
593
|
if (addToRun('--retries=0'))
|
|
554
|
-
changes.push('Added `--retries=0`: a
|
|
594
|
+
changes.push('Added `--retries=0`: a local run walks each failure once; the suite’s retries apply on the deployed proof run.');
|
|
555
595
|
}
|
|
556
596
|
return { command: out, changes };
|
|
557
597
|
}
|
|
@@ -575,20 +615,39 @@ const SUITE_RUN_COMMANDS = [
|
|
|
575
615
|
/^hurl\s+[^|\n]*(?:--test|\.hurl)/,
|
|
576
616
|
];
|
|
577
617
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
618
|
+
* Every statement of a command that starts a whole test suite, in command order.
|
|
619
|
+
*
|
|
620
|
+
* This is the ONE count of suite runs in a command: {@link isSuiteRunCommand} asks it,
|
|
621
|
+
* {@link streamingBackgroundCommand} refuses a command carrying more than one — each statement
|
|
622
|
+
* starts a whole walk of the same suite, and only the last of them is rewritten — and a re-run
|
|
623
|
+
* composed from a command ({@link ./suite-overlap.js} `scopedRerunCommand`) keeps only the first.
|
|
580
624
|
*
|
|
581
|
-
* A
|
|
625
|
+
* A statement that invokes a runner only to print is not one of these
|
|
582
626
|
* ({@link commandRunsNoTests}): `npx playwright test --list` prints the tests it would run and
|
|
583
627
|
* exits, and `npx playwright install`, `vitest list` and `hurl --help` start no test either.
|
|
584
628
|
*
|
|
585
629
|
* @param command - The shell command.
|
|
630
|
+
* @returns One entry per suite-running statement, as the parser read it (empty when none).
|
|
631
|
+
*/
|
|
632
|
+
export function suiteRunStatements(command) {
|
|
633
|
+
if (commandRunsNoTests(command))
|
|
634
|
+
return [];
|
|
635
|
+
return commandSegments(command)
|
|
636
|
+
.filter((segment) => SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)) &&
|
|
637
|
+
// A statement that only PRINTS (`playwright test --list`) is not a walk, so
|
|
638
|
+
// `npx playwright test --list && npx playwright test` is one run, not two.
|
|
639
|
+
!commandRunsNoTests(segment.text))
|
|
640
|
+
.map((segment) => segment.text);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run
|
|
644
|
+
* — rather than a build, an install or a one-off script.
|
|
645
|
+
*
|
|
646
|
+
* @param command - The shell command.
|
|
586
647
|
* @returns True when the command starts one of those runners.
|
|
587
648
|
*/
|
|
588
649
|
export function isSuiteRunCommand(command) {
|
|
589
|
-
|
|
590
|
-
return false;
|
|
591
|
-
return commandSegments(command).some((segment) => SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)));
|
|
650
|
+
return suiteRunStatements(command).length > 0;
|
|
592
651
|
}
|
|
593
652
|
/**
|
|
594
653
|
* The command with the `timeout <N>` wrapper lifted off the statement that runs the suite, and the
|
|
@@ -1953,6 +2012,23 @@ function largestGroup(groups) {
|
|
|
1953
2012
|
function testKey(t) {
|
|
1954
2013
|
return `${t.where}|${t.title}`;
|
|
1955
2014
|
}
|
|
2015
|
+
/**
|
|
2016
|
+
* One test's identity with the runner's retry marker taken off: `… a.spec.ts:32 › does x
|
|
2017
|
+
* (retry #1)` is the same test as `… a.spec.ts:32 › does x`.
|
|
2018
|
+
*
|
|
2019
|
+
* A retried failure prints twice in a `list` log — its first attempt, then the same test with
|
|
2020
|
+
* `(retry #1)` after the title — while its failure block names it without the marker. Keyed
|
|
2021
|
+
* literally, the retry row is a test no cause claims, so it fell through to the duration arm and
|
|
2022
|
+
* was listed as a "shared duration" cause beside the proven cause it repeated: on
|
|
2023
|
+
* xr-molecule-flash-m155 (+21:50) every retry row of the run became one of six extra causes
|
|
2024
|
+
* duplicating the causes above them.
|
|
2025
|
+
*
|
|
2026
|
+
* @param t - A failed test, as a result line or a failure block spelled it.
|
|
2027
|
+
* @returns The key a test's retry row shares with its failure block.
|
|
2028
|
+
*/
|
|
2029
|
+
function retrylessTestKey(t) {
|
|
2030
|
+
return `${t.where}|${t.title.replace(/\s*\(retry #\d+\)$/, '')}`;
|
|
2031
|
+
}
|
|
1956
2032
|
/**
|
|
1957
2033
|
* The same tests with each one kept once, in first-seen order.
|
|
1958
2034
|
*
|
|
@@ -2600,6 +2676,10 @@ function groupFailures(log, excluded, pageErrors = []) {
|
|
|
2600
2676
|
const listed = failedTests(text);
|
|
2601
2677
|
named.push(...listed);
|
|
2602
2678
|
const claimed = new Set(causes.flatMap((c) => c.tests.map(testKey)));
|
|
2679
|
+
// The causes above as a test can recognise itself: a retried failure's second row carries the
|
|
2680
|
+
// runner's `(retry #1)` marker, its failure block does not, and comparing the literal keys
|
|
2681
|
+
// read the same test as an unclaimed one (xr-molecule-flash-m155 +21:50).
|
|
2682
|
+
const proven = new Set(causes.flatMap((c) => c.tests.map(retrylessTestKey)));
|
|
2603
2683
|
const unclaimed = distinctTests(listed).filter((t) => t.seconds !== null &&
|
|
2604
2684
|
!claimed.has(testKey(t)) &&
|
|
2605
2685
|
// A flaky test's `✘` row is its first attempt; its `✓` retry follows it in the same log,
|
|
@@ -2615,14 +2695,21 @@ function groupFailures(log, excluded, pageErrors = []) {
|
|
|
2615
2695
|
else
|
|
2616
2696
|
byDuration.push({ seconds: t.seconds, tests: [t] });
|
|
2617
2697
|
}
|
|
2618
|
-
for (const g of byDuration)
|
|
2698
|
+
for (const g of byDuration) {
|
|
2699
|
+
// A duration band is a LEAD, not a cause: a test already carrying a proven cause above is
|
|
2700
|
+
// not named a second time here, and a band whose every member is one is not a cause at all —
|
|
2701
|
+
// it reads as one more thing to fix. m155's retry rows made six such bands.
|
|
2702
|
+
const tests = g.tests.filter((t) => !proven.has(retrylessTestKey(t)));
|
|
2703
|
+
if (tests.length === 0)
|
|
2704
|
+
continue;
|
|
2619
2705
|
causes.push({
|
|
2620
2706
|
message: null,
|
|
2621
2707
|
seconds: g.seconds,
|
|
2622
|
-
tests
|
|
2708
|
+
tests,
|
|
2623
2709
|
evidence: [],
|
|
2624
2710
|
kind: 'assertion',
|
|
2625
2711
|
});
|
|
2712
|
+
}
|
|
2626
2713
|
const ignored = distinctTests(named).filter((t) => matchingExcludedTitles(t.title, excluded).length > 0);
|
|
2627
2714
|
return {
|
|
2628
2715
|
causes: causes.sort((a, b) => b.tests.length - a.tests.length),
|