@molecule/api-ai-tools 1.6.6 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cut-run.js CHANGED
@@ -24,6 +24,12 @@
24
24
  * `backends/sandbox.ts`), which reports 124 whatever the pipeline did, and returns everything
25
25
  * printed until then. The budget is the tool's own, so its number belongs to the tool.
26
26
  *
27
+ * A runner's closing summary is what separates "used the bound and finished" from "stopped at the
28
+ * bound", but it is only half the evidence: Playwright prints one when its process is stopped too,
29
+ * listing just the tests it reached. So a summary is a finish only when it accounts for the total
30
+ * the runner announced before it started ({@link suiteSummary}) — a summary that does not
31
+ * reconcile is a stopped run like any other, and the result says so.
32
+ *
27
33
  * @module
28
34
  */
29
35
  import { commandTimeoutSeconds } from './command-position.js';
@@ -54,7 +60,8 @@ export function commandRunState(result) {
54
60
  * not a tolerance: a command killed at its bound reports AT LEAST the bound (the shell's own
55
61
  * bookkeeping runs after the kill, and this tool's round trip is on top), so any slack would only
56
62
  * make a SHORT bound fire on a run that finished inside it (`timeout 5 curl …` answering in 3 s).
57
- * A run that finished near its bound carried its runner's closing summary — {@link suiteTally}.
63
+ * A run that finished near its bound carried a summary that reconciles with its announced total —
64
+ * {@link suiteSummary}.
58
65
  *
59
66
  * @param boundSeconds - The bound the run was allowed.
60
67
  * @returns The elapsed ms a run stopped at that bound reports at least.
@@ -62,23 +69,126 @@ export function commandRunState(result) {
62
69
  function atBoundMs(boundSeconds) {
63
70
  return boundSeconds * 1000;
64
71
  }
72
+ /** A runner's announcement of the whole walk — Playwright's `Running 139 tests using 1 worker`. */
73
+ const ANNOUNCED_TOTAL = /^\s*Running\s+(\d+)\s+tests?\s+using\b/gm;
74
+ /**
75
+ * One line of Playwright's end-of-run tally — ` 1 failed`, ` 36 passed (2.5m)`,
76
+ * ` 1 did not run`.
77
+ */
78
+ const SUMMARY_STATUS = /^\s*(\d+)\s+(passed|failed|flaky|skipped|interrupted|timed out|did not run)\b/gm;
79
+ /**
80
+ * Vitest's end-of-run tally — `Tests 1 failed | 149 passed (151)` — whose parenthesised number is
81
+ * the total the line accounts for, and so its own announcement ({@link SuiteSummary.announced}).
82
+ */
83
+ const VITEST_TESTS = /^\s*Tests\s+(.+?)\s+\((\d+)\)\s*$/gm;
84
+ /** One status inside Vitest's `Tests` line — `149 passed`. */
85
+ const VITEST_STATUS = /(\d+)\s+(passed|failed|skipped|todo)\b/g;
86
+ /** Hurl's closing block, printed only by a run that walked its whole file list. */
87
+ const HURL_TALLY = /\bExecuted files:\s*\d+/;
65
88
  /**
66
- * A runner's closing summary, printed only by a run that walked its whole suite: Hurl's
67
- * `Executed files:` block and the `N passed` / `N failed` / `N flaky` line Playwright and Vitest
68
- * print when they finish.
89
+ * The marker {@link truncateMiddle} puts before the tally lines it restores — read only when the
90
+ * truncated text carries no tally line of its own.
91
+ */
92
+ const RESTORED_TALLY = /\[run tally\]\s*([^\n]*)/g;
93
+ /** One status inside a restored tally line — `36 passed`. */
94
+ const RESTORED_STATUS = /(\d+)\s+(passed|failed|flaky|skipped|interrupted|timed out|did not run)\b/g;
95
+ /**
96
+ * Add the statuses in `text` to a running tally.
97
+ *
98
+ * Every status counts the same way: a test the runner skipped or left "did not run" was still
99
+ * reached, which is what a stopped run's count measures.
69
100
  *
70
- * This is what separates "stopped at the bound" from "used the bound and finished": a suite that
71
- * printed its summary ran to its end, and is read as the run it was.
101
+ * @param text - A tally line, or a block of them.
102
+ * @param re - The status pattern to read them with.
103
+ * @param counts - The tally to add to.
72
104
  */
73
- const SUITE_TALLY = /\bExecuted files:\s*\d+|\b\d+\s+(?:passed|failed|flaky|skipped)\b/;
105
+ function addStatuses(text, re, counts) {
106
+ for (const m of text.matchAll(re)) {
107
+ counts.saw = true;
108
+ counts.accounted += Number(m[1]);
109
+ }
110
+ }
74
111
  /**
75
- * Whether text carries a runner's closing summary ({@link SUITE_TALLY}).
112
+ * What the runner's closing summary accounts for, or null when it printed none.
113
+ *
114
+ * Three runners' summaries are read, each by its own wording: Playwright's per-status lines
115
+ * (reconciled against its `Running N tests` announcement), Vitest's single `Tests …` line (its own
116
+ * parenthesised total), and Hurl's `Executed files:` block (which it prints only at the end, and
117
+ * which announces no total — so it is a finish by construction).
118
+ *
119
+ * @param text - The run's stdout and stderr, RAW (a truncated copy can lose the announcement).
120
+ * @returns The summary, or null when the text carries none.
121
+ */
122
+ export function suiteSummary(text) {
123
+ if (typeof text !== 'string' || !text)
124
+ return null;
125
+ let announced = null;
126
+ for (const m of text.matchAll(ANNOUNCED_TOTAL))
127
+ announced = Number(m[1]);
128
+ const counts = { accounted: 0, saw: false };
129
+ addStatuses(text, SUMMARY_STATUS, counts);
130
+ if (!counts.saw) {
131
+ // Only when no tally line of its own survived: the restored line is a copy of lines that may
132
+ // also be present, and adding both would over-count a cut run into a finish.
133
+ for (const m of text.matchAll(RESTORED_TALLY))
134
+ addStatuses(m[1] ?? '', RESTORED_STATUS, counts);
135
+ }
136
+ if (counts.saw) {
137
+ return {
138
+ announced,
139
+ accounted: counts.accounted,
140
+ finished: announced === null || counts.accounted >= announced,
141
+ };
142
+ }
143
+ const vitest = [...text.matchAll(VITEST_TESTS)].at(-1);
144
+ if (vitest) {
145
+ const total = Number(vitest[2]);
146
+ const line = { accounted: 0, saw: true };
147
+ addStatuses(vitest[1] ?? '', VITEST_STATUS, line);
148
+ return { announced: total, accounted: line.accounted, finished: line.accounted >= total };
149
+ }
150
+ // Hurl prints `Executed files:` only after walking its whole list.
151
+ if (HURL_TALLY.test(text))
152
+ return { announced: null, accounted: 0, finished: true };
153
+ return null;
154
+ }
155
+ /**
156
+ * Whether text carries a runner's closing summary.
157
+ *
158
+ * A summary is NOT a finish on its own — a stopped Playwright run prints one too. Use
159
+ * {@link suiteSummary} and its `finished` for that.
76
160
  *
77
161
  * @param text - The run's stdout and stderr.
78
162
  * @returns True when the runner's own end-of-run summary is there.
79
163
  */
80
164
  export function suiteTally(text) {
81
- return SUITE_TALLY.test(text);
165
+ return suiteSummary(text) !== null;
166
+ }
167
+ /** A failure count in a runner's own tally — `1 failed`, `Tests 1 failed | 149 passed (151)`. */
168
+ const FAILED_TALLY = /\b[1-9]\d*\s+failed\b/;
169
+ /**
170
+ * The exit code a finished suite run may report.
171
+ *
172
+ * The code in a background result is the code of the command's LAST pipeline stage. When a stage
173
+ * after the suite runner still consumes its output (`| tee run.log`, or a filter the platform could
174
+ * not prove output-only), that stage produced the code, not the run — and `… | grep … | tail -20`
175
+ * exited 0 for a run with a failure in it (xr-molecule-flash-m101 +25:54). No code is reported
176
+ * then: the log's own tally decides.
177
+ *
178
+ * The same reading covers a code of 0 beside a tally that names a failure: whatever ended the
179
+ * pipeline, a run whose log counts a failure did not pass.
180
+ *
181
+ * @param exitCode - The code the pipeline reported (null when it has none).
182
+ * @param output - The run's full log.
183
+ * @param piped - Whether a pipeline stage after the suite runner still consumes its output.
184
+ * @returns The code the run may report, or null when the code is the filter's or contradicts the tally.
185
+ */
186
+ export function suiteRunExitCode(exitCode, output, piped) {
187
+ if (piped)
188
+ return null;
189
+ if (exitCode === 0 && FAILED_TALLY.test(output))
190
+ return null;
191
+ return exitCode;
82
192
  }
83
193
  /** The last thing a run printed: its last non-blank line, as one line and short enough to quote. */
84
194
  function lastPrinted(text) {
@@ -98,19 +208,26 @@ const PROGRESS_LINE = /^\s*\[(\d+)\/(\d+)\]/gm;
98
208
  * What the runner's own output says the run got through.
99
209
  *
100
210
  * Hurl prints a line per finished file and no total unless it finished; Playwright's line reporter
101
- * prints `[n/K]` with both numbers. Neither is inferred from the command — a run that printed
102
- * nothing says nothing, which is what `null` means here.
211
+ * prints `[n/K]` with both numbers, and its end-of-run summary (with the `Running N tests` total it
212
+ * is reconciled against) says how far a stopped walk got. Neither is inferred from the command — a
213
+ * run that printed nothing says nothing, which is what `null` means here.
103
214
  *
104
- * @param text - The run's stdout and stderr.
215
+ * @param text - The run's stdout and stderr, raw.
105
216
  * @param suite - Whether the command runs a suite.
106
- * @returns How many finished, how many the run was walking, and the noun.
217
+ * @param summary - The closing summary the text carries, when it carries one.
218
+ * @returns How far it got, how many it was walking, and the noun.
107
219
  */
108
- function runProgress(text, suite) {
220
+ function runProgress(text, suite, summary) {
109
221
  const files = new Set();
110
222
  for (const m of text.matchAll(HURL_FILE_LINE))
111
223
  files.add(m[1] ?? '');
112
224
  if (files.size > 0)
113
225
  return { ran: files.size, total: null, noun: 'files' };
226
+ // A summary that does not account for its announced total: both numbers come from the runner's
227
+ // own lines, so the sentence can say `Ran 38 of 139` without the caller counting anything.
228
+ if (summary && summary.announced !== null) {
229
+ return { ran: summary.accounted, total: summary.announced, noun: 'tests' };
230
+ }
114
231
  let ran = null;
115
232
  let total = null;
116
233
  for (const m of text.matchAll(PROGRESS_LINE)) {
@@ -127,6 +244,9 @@ function runProgress(text, suite) {
127
244
  *
128
245
  * Evidence, in order — never a guess:
129
246
  *
247
+ * - the runner's own summary, when it printed one: it is a finish only if it accounts for the total
248
+ * the runner announced. A summary that accounts for fewer is a walk a process stopped early, and
249
+ * falls through to the branches below with its own counts ({@link suiteSummary}).
130
250
  * - `timeout`'s own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired,
131
251
  * whatever the pipeline ended with) — and, when the clock says the command's own smaller bound
132
252
  * ran out first and less than the budget, that bound instead.
@@ -135,14 +255,19 @@ function runProgress(text, suite) {
135
255
  * killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's.
136
256
  * - the run used this tool's whole budget behind a filter that withheld its output, and printed no
137
257
  * closing summary: the budget stopped it, and no code in the result is the run's.
258
+ * - a summary that does not account for its announced total, with neither bound fired: something
259
+ * outside this platform stopped the process ({@link CutBy} `process-stop`).
138
260
  *
139
261
  * @param command - The command the executor sent.
140
262
  * @param result - Its result.
141
263
  * @param elapsedMs - How long the command ran, in ms.
142
264
  * @param budgetSeconds - This tool's foreground budget, in seconds (0 when it has none).
265
+ * @param rawOutput - The run's output BEFORE truncation, when the caller has it: the announcement
266
+ * a summary is reconciled against is the first line a long run prints, which is exactly what a
267
+ * size cap drops. Detection reads this; the sentence's last line still comes from `result`.
143
268
  * @returns The stopped run, or null.
144
269
  */
145
- export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0) {
270
+ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, rawOutput) {
146
271
  if (typeof command !== 'string' || !command.trim())
147
272
  return null;
148
273
  if (result === null || typeof result !== 'object')
@@ -152,12 +277,18 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0) {
152
277
  const r = result;
153
278
  const exitCode = typeof r.exitCode === 'number' ? r.exitCode : null;
154
279
  const text = `${String(r.stdout ?? '')}\n${String(r.stderr ?? '')}`;
155
- // A suite that printed its summary ran to its end, whatever it was wrapped in.
156
- if (suiteTally(text))
280
+ const detected = typeof rawOutput === 'string' ? rawOutput : text;
281
+ // A runner's summary is a finish only when it accounts for the total the runner announced. A run
282
+ // stopped at 36 of 139 prints the same `36 passed` line a finished run does (X1 xr-molecule-flash-
283
+ // m101 +12:13 read `36 passed` as `exitCode: 0` and moved on), so the announcement decides.
284
+ const summary = suiteSummary(detected);
285
+ // Only a summary that ACCOUNTS for the announced total ends the search for a stop. No summary
286
+ // at all is not a finish — it is the case the bounds below exist for.
287
+ if (summary?.finished)
157
288
  return null;
158
289
  const suite = isSuiteRunCommand(command);
159
290
  const withheld = /\|\s*(?:tail|head|grep|egrep|fgrep|sort|uniq|wc|jq)\b/.test(command);
160
- const progress = runProgress(text, suite);
291
+ const progress = runProgress(detected, suite, summary);
161
292
  const doing = lastPrinted(text);
162
293
  const declared = commandTimeoutSeconds(command);
163
294
  // A command whose own bound is inside this tool's budget is stopped by ITS bound: a larger one
@@ -203,6 +334,21 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0) {
203
334
  doing,
204
335
  };
205
336
  }
337
+ // The runner printed a summary that does not account for the total it announced, and neither
338
+ // bound above fired: the process was stopped from outside the run. A run that used the whole
339
+ // budget is the backend's deadline (which sits just past it); anything else is a stop this tool
340
+ // does not own. Either way the summary counts are all that ran, and a pipeline's 0 is not the
341
+ // run's code — the run never reached its own verdict.
342
+ if (summary !== null && summary.announced !== null) {
343
+ const usedBudget = budgetSeconds > 0 && elapsedMs >= atBoundMs(budgetSeconds);
344
+ return {
345
+ seconds: usedBudget ? budgetSeconds : Math.max(1, Math.round(elapsedMs / 1000)),
346
+ stoppedBy: usedBudget ? 'tool-budget' : 'process-stop',
347
+ exitCode: withheld || exitCode === 0 ? null : exitCode,
348
+ ...progress,
349
+ doing,
350
+ };
351
+ }
206
352
  return null;
207
353
  }
208
354
  /**
@@ -239,29 +385,36 @@ export function cutRunFacts(result) {
239
385
  };
240
386
  }
241
387
  /**
242
- * The first line of a stopped run's result: how long it was allowed, what stopped it, how far it
243
- * got, and the last thing it printed.
388
+ * The first line of a stopped run's result: how far it got, what stopped it, the last thing it
389
+ * printed, and what the reader must do next.
244
390
  *
245
391
  * Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
246
392
  * the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
247
- * read elsewhere (the suite's own file count) fills in the "M of K" it can then state.
393
+ * read elsewhere (the suite's own file count) fills in the "of N" it can then state. The sentence
394
+ * says how many tests the stop left unrun — a number a reader cannot round up to "the suite
395
+ * passed" — and hands over the one move that survives a long run: run it in the background.
248
396
  *
249
397
  * @param run - The stopped run.
250
398
  * @returns The sentence.
251
399
  */
252
400
  export function cutRunSentence(run) {
253
- const by = run.stoppedBy === 'tool-budget' ? "this tool's limit" : `its own \`timeout ${run.seconds}\``;
254
- const lead = `This run was stopped after ${run.seconds}s (${by})`;
255
- const count = run.ran !== null && run.total !== null
256
- ? `: ${run.ran} of ${run.total} ${run.noun} ran, the rest did not run`
401
+ const by = run.stoppedBy === 'tool-budget'
402
+ ? "this tool's limit"
403
+ : run.stoppedBy === 'command-timeout'
404
+ ? `its own \`timeout ${run.seconds}\``
405
+ : 'a process kill outside this tool (no `timeout` of its own and no limit of this tool had fired)';
406
+ const background = 'run it in the background (run_in_background + wait_for_task)';
407
+ const missing = run.ran !== null && run.total !== null ? run.total - run.ran : null;
408
+ const lead = missing !== null
409
+ ? `Ran ${run.ran} of ${run.total} ${run.noun} in ${run.seconds} s and was stopped by ${by} — the ${missing} it never reached ${missing === 1 ? 'has' : 'have'} not run; ${background}`
257
410
  : run.ran !== null
258
- ? `: ${run.ran} ${run.noun} ran and the rest did not run`
259
- : ' before it finished';
260
- const doing = run.doing ? `; the last thing it was doing: ${run.doing}` : '';
411
+ ? `Ran ${run.ran} ${run.noun} in ${run.seconds} s and was stopped by ${by} — the rest have not run; ${background}`
412
+ : `This run was stopped after ${run.seconds}s (${by}) before it finished; ${background}`;
413
+ const doing = run.doing ? ` The last thing it was doing: ${run.doing}.` : '';
261
414
  // Why the result carries no code: without this the model reads the missing field as an
262
415
  // omission, and the number it used to read there was the pipeline's own 0.
263
416
  const code = run.exitCode === null
264
417
  ? ' It has no exit code of its own to report — the one this pipeline produced belongs to the filter, not to the run.'
265
418
  : '';
266
- return `${lead}${count}${doing}.${code}`;
419
+ return `${lead}.${doing}${code}`;
267
420
  }
package/dist/index.d.ts CHANGED
@@ -140,6 +140,7 @@ export * from './schemas.js';
140
140
  export * from './suite-overlap.js';
141
141
  export * from './system-prompt.js';
142
142
  export * from './tools.js';
143
+ export * from './trace-artifacts.js';
143
144
  export * from './types.js';
144
145
  export * from './utilities.js';
145
146
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -140,5 +140,6 @@ export * from './schemas.js';
140
140
  export * from './suite-overlap.js';
141
141
  export * from './system-prompt.js';
142
142
  export * from './tools.js';
143
+ export * from './trace-artifacts.js';
143
144
  export * from './types.js';
144
145
  export * from './utilities.js';
@@ -60,9 +60,13 @@ export declare function suiteCommandTarget(command: string): string;
60
60
  * @param locations - The failing `file:line`s to re-run, as the run's own log printed them — or,
61
61
  * for a run that started outside the suite, the absolute form Playwright matches each file
62
62
  * argument against.
63
+ * @param excluded - The test titles the request carves out. They are inverted out
64
+ * (`--grep-invert`) so a re-run of the failing locations cannot report one of them again —
65
+ * xr-molecule-flash-m101 re-ran the whole 139-test suite and spent ~4 minutes to be told about
66
+ * a test the request had already said does not count.
63
67
  * @returns The scoped command, or the command unchanged when it runs no Playwright suite.
64
68
  */
65
- export declare function scopedRerunCommand(command: string, locations: readonly string[]): string;
69
+ export declare function scopedRerunCommand(command: string, locations: readonly string[], excluded?: readonly string[]): string;
66
70
  /**
67
71
  * One suite run, as the rule that decides whether a second may start beside it reads it.
68
72
  *
@@ -1 +1 @@
1
- {"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAexF;AAED;;;;;;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"}
1
+ {"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,QAAQ,GAAE,SAAS,MAAM,EAAO,GAC/B,MAAM,CA2BR;AAED;;;;;;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"}
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { commandSegments } from './command-position.js';
32
32
  import { commandRunsNoTests } from './no-test-run.js';
33
+ import { shellQuote } from './utilities.js';
33
34
  /**
34
35
  * A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it
35
36
  * WRITES are not what the command itself does.
@@ -83,9 +84,13 @@ export function suiteCommandTarget(command) {
83
84
  * @param locations - The failing `file:line`s to re-run, as the run's own log printed them — or,
84
85
  * for a run that started outside the suite, the absolute form Playwright matches each file
85
86
  * argument against.
87
+ * @param excluded - The test titles the request carves out. They are inverted out
88
+ * (`--grep-invert`) so a re-run of the failing locations cannot report one of them again —
89
+ * xr-molecule-flash-m101 re-ran the whole 139-test suite and spent ~4 minutes to be told about
90
+ * a test the request had already said does not count.
86
91
  * @returns The scoped command, or the command unchanged when it runs no Playwright suite.
87
92
  */
88
- export function scopedRerunCommand(command, locations) {
93
+ export function scopedRerunCommand(command, locations, excluded = []) {
89
94
  const at = command.search(/\bplaywright\s+test\b/);
90
95
  if (at === -1)
91
96
  return command;
@@ -96,10 +101,19 @@ export function scopedRerunCommand(command, locations) {
96
101
  const end = rest.search(/&&|\|\||;|\|/);
97
102
  const statement = end === -1 ? rest : rest.slice(0, end);
98
103
  const tail = end === -1 ? '' : rest.slice(end);
104
+ // One literal alternation, regex-escaped and shell-quoted: a title carries parentheses and
105
+ // quotes (`should only allow comment author to delete`) that are regex and shell syntax alike.
106
+ const invert = excluded.length > 0 && !/--grep-invert\b/.test(command)
107
+ ? ` --grep-invert ${shellQuote(excluded
108
+ .map((t) => t.replace(/\s+/g, ' ').trim())
109
+ .filter(Boolean)
110
+ .map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
111
+ .join('|'))}`
112
+ : '';
99
113
  const scoped = statement
100
114
  .replace(/\s*--last-failed\b/g, '')
101
115
  .replace(/\s*--retries(?:[= ]\d+)?/g, '')
102
- .replace(/^(playwright\s+test)\b/, `$1 ${locations.join(' ')} --retries=0`);
116
+ .replace(/^(playwright\s+test)\b/, `$1 ${locations.join(' ')} --retries=0${invert}`);
103
117
  return `${head}${scoped}${tail}`;
104
118
  }
105
119
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkF9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAwHD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAgEnE;;;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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBxE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAipExF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA0G9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAyHD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAkEnE;;;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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBxE;AAgDD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAyzExF"}