@molecule/api-ai-tools 1.7.0 → 1.8.2
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 +324 -25
- package/dist/command-position.d.ts +42 -0
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +271 -41
- package/dist/cut-run.d.ts +98 -8
- package/dist/cut-run.d.ts.map +1 -1
- package/dist/cut-run.js +182 -29
- package/dist/suite-overlap.d.ts +5 -1
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +16 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +78 -13
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +45 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities.d.ts +121 -8
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +482 -41
- package/package.json +1 -1
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
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
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
|
-
*
|
|
71
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
102
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
156
|
-
|
|
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(
|
|
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
|
|
243
|
-
*
|
|
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 "
|
|
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'
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
?
|
|
259
|
-
:
|
|
260
|
-
const 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}
|
|
419
|
+
return `${lead}.${doing}${code}`;
|
|
267
420
|
}
|
package/dist/suite-overlap.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/suite-overlap.js
CHANGED
|
@@ -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
|
/**
|
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;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;
|
|
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"}
|
package/dist/tools.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { commandCutRun, CUT_AT_BUDGET, cutRunSentence } from './cut-run.js';
|
|
9
|
+
import { commandCutRun, CUT_AT_BUDGET, cutRunSentence, suiteRunExitCode } from './cut-run.js';
|
|
10
10
|
import { addChangeSpan, changedSpan, countLines, renderEditExcerpt, renderSmallFile, } from './edit-excerpt.js';
|
|
11
11
|
import { guardToolExecute, missingParamError, ToolInputError } from './input-normalizer.js';
|
|
12
12
|
import { TOOL_SCHEMAS } from './schemas.js';
|
|
@@ -36,6 +36,14 @@ const outputFileByTask = new Map();
|
|
|
36
36
|
* resolves the failing test's `file:line` against the wrong suite.
|
|
37
37
|
*/
|
|
38
38
|
const runByTask = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Why a finished suite run's result carries no exit code: the code a pipeline reports is its last
|
|
41
|
+
* stage's, and a stage after the runner (or a code of 0 beside a failure in the tally) is not the
|
|
42
|
+
* run's verdict. Said in the result so the missing field does not read as an omission.
|
|
43
|
+
*/
|
|
44
|
+
const FILTERED_EXIT_NOTE = 'No exit code is reported for this run: a stage after the test runner produced the pipeline’s ' +
|
|
45
|
+
'code (or the code says 0 beside a failure in the tally), so read the pass/fail tally in the ' +
|
|
46
|
+
'log instead.';
|
|
39
47
|
/**
|
|
40
48
|
* Task ids whose own-output file has already been looked for this process.
|
|
41
49
|
* {@link outputFileByTask} is empty for every task after a restart — it is
|
|
@@ -222,7 +230,7 @@ function runningNote(clauses) {
|
|
|
222
230
|
return `${joined[0].toUpperCase()}${joined.slice(1)}.`;
|
|
223
231
|
}
|
|
224
232
|
import { diagnosisTraceCommand } from './trace-artifacts.js';
|
|
225
|
-
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, earlyReturnCause, emptyCallError, failureCausesNote, findPathArgs, findPatternArgs, fusedLine, htmlOutline, isCommandTimeout, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isSuiteRunCommand, isValidGlob, linesWithinSerialized, literalDirPrefix, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, searchTimeoutError, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
|
|
233
|
+
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, earlyReturnCause, emptyCallError, failureCausesNote, findPathArgs, findPatternArgs, fusedLine, htmlOutline, isCommandTimeout, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isSuiteRunCommand, isValidGlob, linesWithinSerialized, literalDirPrefix, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, searchTimeoutError, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPipedAfterRunner, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
|
|
226
234
|
/**
|
|
227
235
|
* How long exec_command waits for a command it backgrounded only because the
|
|
228
236
|
* command asked for more than the tool's ceiling, before handing back a task id.
|
|
@@ -377,7 +385,7 @@ function batchFailureError(path, total, failures) {
|
|
|
377
385
|
* @returns Array of AITool objects ready to pass to an AI provider
|
|
378
386
|
*/
|
|
379
387
|
export function buildTools(backend, config) {
|
|
380
|
-
const { include, exclude, pathGuards = true, symlinkGuards = false, redactSecrets: doRedact = true, blockDangerousCommands = false, blockCommand, execTimeoutMs = 120_000, commandBudgetMs, overCeilingGraceMs = OVER_CEILING_GRACE_MS, searchExcludedDirs, outlineLargeHtml, onAfterWrite, onFileDiff, onFileChange, } = config ?? {};
|
|
388
|
+
const { include, exclude, pathGuards = true, symlinkGuards = false, redactSecrets: doRedact = true, blockDangerousCommands = false, blockCommand, execTimeoutMs = 120_000, commandBudgetMs, overCeilingGraceMs = OVER_CEILING_GRACE_MS, searchExcludedDirs, outlineLargeHtml, excludedTestTitles, onAfterWrite, onFileDiff, onFileChange, } = config ?? {};
|
|
381
389
|
const root = backend.projectRoot;
|
|
382
390
|
// A host-filesystem backend with escape guards off is the combination the
|
|
383
391
|
// audit flagged: `pathGuards` off lets `..` paths out of the workspace,
|
|
@@ -1879,6 +1887,9 @@ export function buildTools(backend, config) {
|
|
|
1879
1887
|
outputFileByTask.set(id, ownFile);
|
|
1880
1888
|
runByTask.set(id, { command: streaming.command, cwd });
|
|
1881
1889
|
await backend.writeFile(script, `${streaming.command}\n`);
|
|
1890
|
+
// The clock the grace return below reports a stopped run against: the command is
|
|
1891
|
+
// started here, and how long it ran is what a stop at its own bound is read from.
|
|
1892
|
+
const launchedAt = Date.now();
|
|
1882
1893
|
await backend.run(`nohup sh -c ${shellQuote(prioritized.launch)} ` +
|
|
1883
1894
|
`> /dev/null 2>&1 & echo $! > /tmp/${id}.pid`, { cwd, timeout: 15_000 });
|
|
1884
1895
|
// A command backgrounded only for the budget it ASKED for often ends in
|
|
@@ -1908,12 +1919,43 @@ export function buildTools(backend, config) {
|
|
|
1908
1919
|
// Finished without writing anything: an empty output is still a result.
|
|
1909
1920
|
output = '';
|
|
1910
1921
|
}
|
|
1922
|
+
const stdout = sanitizeOutput(truncateMiddle(output, MAX_OUTPUT_SIZE));
|
|
1923
|
+
// A grace return is a foreground-shaped result, so it carries the same stop: this
|
|
1924
|
+
// command was started because its output is withheld until it ends (a Playwright
|
|
1925
|
+
// run behind a pipe), and a run that printed a tally accounting for fewer tests
|
|
1926
|
+
// than it announced was stopped — not "Finished … with exit code 0" (the pipeline's
|
|
1927
|
+
// 0 is the filter's). No tool budget applies to a detached run, so the command's
|
|
1928
|
+
// own `timeout` is the only bound here.
|
|
1929
|
+
const cut = commandCutRun(command, { exitCode: code, stdout, stderr: '' }, Date.now() - launchedAt, 0, output);
|
|
1930
|
+
if (cut) {
|
|
1931
|
+
return {
|
|
1932
|
+
stdout,
|
|
1933
|
+
stderr: '',
|
|
1934
|
+
status: CUT_AT_BUDGET,
|
|
1935
|
+
exitCode: cut.exitCode,
|
|
1936
|
+
cutRun: cut,
|
|
1937
|
+
...(output.length > MAX_OUTPUT_SIZE
|
|
1938
|
+
? { truncated: true, fullOutput: log }
|
|
1939
|
+
: {}),
|
|
1940
|
+
error: `${cutRunSentence(cut)} ` +
|
|
1941
|
+
(isSuiteRunCommand(command) ? SUITE_OVER_BUDGET_ADVICE : SPLIT_WORK_ADVICE),
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
// The exit file holds the PIPELINE's code: when a stage this rewrite could not
|
|
1945
|
+
// prove is output-only still follows the suite runner (`| tee run.log`), that code
|
|
1946
|
+
// is the filter's — report none and let the log's tally decide (m101 +25:54).
|
|
1947
|
+
const piped = suiteRunPipedAfterRunner(streaming.command);
|
|
1948
|
+
const reported = suiteRunExitCode(code, output, piped);
|
|
1949
|
+
const notes = [
|
|
1950
|
+
...streaming.changes,
|
|
1951
|
+
...(reported === null ? [FILTERED_EXIT_NOTE] : []),
|
|
1952
|
+
];
|
|
1911
1953
|
return {
|
|
1912
|
-
stdout
|
|
1954
|
+
stdout,
|
|
1913
1955
|
stderr: '',
|
|
1914
|
-
exitCode:
|
|
1956
|
+
exitCode: reported,
|
|
1915
1957
|
...(output.length > MAX_OUTPUT_SIZE ? { truncated: true, fullOutput: log } : {}),
|
|
1916
|
-
...(
|
|
1958
|
+
...(notes.length ? { note: notes.join(' ') } : {}),
|
|
1917
1959
|
};
|
|
1918
1960
|
}
|
|
1919
1961
|
await new Promise((r) => setTimeout(r, 1_500));
|
|
@@ -2005,7 +2047,12 @@ export function buildTools(backend, config) {
|
|
|
2005
2047
|
// output and the clock in one place (cut-run.ts) so every reader of a run's verdict reads
|
|
2006
2048
|
// the same state (X1 xr-molecule-flash-m93: `timeout 280 hurl … | tail -60` came back
|
|
2007
2049
|
// `exitCode: 0` after 284 s and four of thirteen files, and the executor re-sent it).
|
|
2008
|
-
const cut = commandCutRun(command, { exitCode: result.exitCode, stdout, stderr }, elapsedMs, budgetSeconds
|
|
2050
|
+
const cut = commandCutRun(command, { exitCode: result.exitCode, stdout, stderr }, elapsedMs, budgetSeconds,
|
|
2051
|
+
// Detection reads the RAW output: `truncateMiddle` keeps the head and the tail, and the
|
|
2052
|
+
// runner's announcement (`Running 139 tests using 1 worker`) is the first line a long run
|
|
2053
|
+
// prints — the one a size cap drops first. Only the counts and the tally come from here;
|
|
2054
|
+
// what the result quotes is still the sanitized, truncated text.
|
|
2055
|
+
`${result.stdout}\n${result.stderr}`);
|
|
2009
2056
|
if (cut) {
|
|
2010
2057
|
const cutLead = cutRunSentence(cut);
|
|
2011
2058
|
return {
|
|
@@ -2400,16 +2447,27 @@ export function buildTools(backend, config) {
|
|
|
2400
2447
|
// flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a single cause, and
|
|
2401
2448
|
// the second and third were found by runs stopped before they walked the rest. Null for
|
|
2402
2449
|
// anything that is not a run with failures in it.
|
|
2403
|
-
|
|
2450
|
+
// How this run was started, so the note can hand over the narrowed re-run of exactly its
|
|
2451
|
+
// failing tests instead of leaving the executor to re-run the whole suite (xr-molecule-
|
|
2452
|
+
// flash-m101 +12:13: 139 tests, ~4 minutes, to check two fixes).
|
|
2453
|
+
const run = runByTask.get(taskId);
|
|
2454
|
+
// The tests the request excludes are not causes to fix, and the re-run below inverts them
|
|
2455
|
+
// out — a result must never ask the executor to fix a test it also says does not count.
|
|
2456
|
+
// The log's own path rides along so a recap that leaves causes out can hand over the ONE
|
|
2457
|
+
// grep that prints their bodies: the stdout above is cut in the middle, so the log is where
|
|
2458
|
+
// they still are whole.
|
|
2459
|
+
const causes = failureCausesNote(output, excludedTestTitles?.() ?? [], run ? { ...run, log: ownFile ?? log } : undefined);
|
|
2404
2460
|
// A backgrounded run is under no tool budget, and the exit file hands back the PIPELINE's
|
|
2405
2461
|
// code — so a suite killed at a `timeout 280` the command carried behind a `| tail` reports
|
|
2406
2462
|
// 0 here too. The run's start is in its own id ({@link taskStartedAt}), so the bound it used
|
|
2407
2463
|
// is readable without new state: say stopped, and never hand the pipeline's 0 back as the
|
|
2408
2464
|
// run's (X1 xr-molecule-flash-m93 re-sent that run three times).
|
|
2409
|
-
const run = runByTask.get(taskId);
|
|
2410
2465
|
const startedAtMs = taskStartedAt(taskId);
|
|
2411
2466
|
const cut = run && startedAtMs !== null
|
|
2412
|
-
? commandCutRun(run.command, { exitCode, stdout: output, stderr: '' }, Date.now() - startedAtMs, 0
|
|
2467
|
+
? commandCutRun(run.command, { exitCode, stdout: output, stderr: '' }, Date.now() - startedAtMs, 0,
|
|
2468
|
+
// Same reason as the foreground path: the log is raw here, and the returned stdout
|
|
2469
|
+
// below is the truncated copy — detection must not read that copy.
|
|
2470
|
+
output)
|
|
2413
2471
|
: null;
|
|
2414
2472
|
if (cut) {
|
|
2415
2473
|
return {
|
|
@@ -2424,12 +2482,19 @@ export function buildTools(backend, config) {
|
|
|
2424
2482
|
};
|
|
2425
2483
|
}
|
|
2426
2484
|
// Same shaping as a foreground exec result, so the executor reads both
|
|
2427
|
-
// the same way.
|
|
2485
|
+
// the same way. The exit file holds the PIPELINE's code: when a stage this rewrite could not
|
|
2486
|
+
// prove is output-only still follows the suite runner (`| tee run.log`), that code is the
|
|
2487
|
+
// filter's — report none and let the log's tally decide (m101 +25:54).
|
|
2488
|
+
const piped = run ? suiteRunPipedAfterRunner(run.command) : false;
|
|
2489
|
+
const reported = suiteRunExitCode(exitCode, output, piped);
|
|
2490
|
+
const noCodeNote = reported === null ? FILTERED_EXIT_NOTE : null;
|
|
2428
2491
|
return {
|
|
2429
2492
|
taskId,
|
|
2430
|
-
exitCode,
|
|
2493
|
+
exitCode: reported,
|
|
2431
2494
|
waitedMs,
|
|
2432
|
-
...(causes
|
|
2495
|
+
...(causes || noCodeNote
|
|
2496
|
+
? { note: [causes, noCodeNote].filter((n) => Boolean(n)).join(' ') }
|
|
2497
|
+
: {}),
|
|
2433
2498
|
stdout: sanitizeOutput(truncateMiddle(output, MAX_OUTPUT_SIZE)),
|
|
2434
2499
|
...(output.length > MAX_OUTPUT_SIZE ? { truncated: true, fullOutput: log } : {}),
|
|
2435
2500
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;
|
|
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,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;CACf;AAED,iGAAiG;AACjG,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,0DAA0D;IAC1D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,iDAAiD;AACjD,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,QAAO,MA+LvC,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAyBnE;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4B/F;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,8BAA8B,CAAA;AAuG/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,CAyBjC"}
|
package/dist/trace-artifacts.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* @module
|
|
28
28
|
*/
|
|
29
|
+
import { commandSegments } from './command-position.js';
|
|
29
30
|
import { isDiagnosisRun } from './suite-overlap.js';
|
|
30
31
|
import { isPlaywrightRunCommand } from './utilities.js';
|
|
31
32
|
/** How many traces one read reports, newest first. */
|
|
@@ -374,6 +375,39 @@ export const DIAGNOSIS_TRACE_FLAG = '--trace=retain-on-failure';
|
|
|
374
375
|
function namesTraceMode(command) {
|
|
375
376
|
return /(?:^|[\s;&|(])--trace(?:=|\s)/.test(command);
|
|
376
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Where the `test` word of the command's Playwright run ends, or null when it has none that the
|
|
380
|
+
* shell parse can place.
|
|
381
|
+
*
|
|
382
|
+
* The flag has to land INSIDE the Playwright step, not at the end of the whole command: a run
|
|
383
|
+
* Flash sends almost every time ends `… 2>&1 | tail -60` or `… > log 2>&1; echo "exit=$?";
|
|
384
|
+
* grep -c "" log`, and a flag appended after those is an operand of `tail`/`grep` — GNU `tail`
|
|
385
|
+
* refuses it outright (`tail: option used in invalid context -- 6`) and Playwright never sees it
|
|
386
|
+
* (xr-molecule-flash-m102 +07:40 → +07:53).
|
|
387
|
+
*
|
|
388
|
+
* The step is found by the package's own shell parse ({@link commandSegments}), the one every
|
|
389
|
+
* runner classifier reads: it answers which SEGMENT runs `playwright test` in command position,
|
|
390
|
+
* so a `playwright test` named in an argument (`grep -n "playwright test" e2e/README.md`) is not
|
|
391
|
+
* a run and is left alone. Each segment's text is its program and arguments re-joined with single
|
|
392
|
+
* spaces, so its offset in the command is found by locating that text, walking the segments in
|
|
393
|
+
* order. A segment whose text cannot be located verbatim (irregular whitespace) ends the walk —
|
|
394
|
+
* null, and the caller adds nothing rather than splicing into a guess.
|
|
395
|
+
*
|
|
396
|
+
* @param command - The shell command.
|
|
397
|
+
* @returns The offset just after `test`, or null.
|
|
398
|
+
*/
|
|
399
|
+
function playwrightTestWordEnd(command) {
|
|
400
|
+
let cursor = 0;
|
|
401
|
+
for (const segment of commandSegments(command)) {
|
|
402
|
+
const at = command.indexOf(segment.text, cursor);
|
|
403
|
+
if (at < 0)
|
|
404
|
+
return null;
|
|
405
|
+
cursor = at + segment.text.length;
|
|
406
|
+
if (/^playwright\s+test\b/.test(segment.text))
|
|
407
|
+
return at + 'playwright test'.length;
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
377
411
|
/** The config paths a candidate directory could hold, in the order Playwright itself looks. */
|
|
378
412
|
const CONFIG_NAMES = [
|
|
379
413
|
'playwright.config.ts',
|
|
@@ -458,8 +492,18 @@ export async function diagnosisTraceCommand(args) {
|
|
|
458
492
|
const config = await readRunConfig(args.command, args.cwd, args.readFile);
|
|
459
493
|
if (suiteRecordsTraces(config))
|
|
460
494
|
return unchanged;
|
|
495
|
+
// The flag goes into the Playwright step, right after its `test` word — an option of the run
|
|
496
|
+
// itself, whatever the command opens with and wherever it pipes or redirects its output. Appended
|
|
497
|
+
// at the end of the whole command it belonged to the LAST pipeline stage instead (`tail`, `grep`),
|
|
498
|
+
// which either refused it or silently ran without it (xr-molecule-flash-m102 +07:40 → +07:53). The
|
|
499
|
+
// step's shape is untouched otherwise, so the rewrites that run on this command afterwards still
|
|
500
|
+
// see what they expect: a trailing `| tail -60` is still trailing and is still dropped
|
|
501
|
+
// (`streamingBackgroundCommand`).
|
|
502
|
+
const at = playwrightTestWordEnd(args.command);
|
|
503
|
+
if (at === null)
|
|
504
|
+
return unchanged;
|
|
461
505
|
return {
|
|
462
|
-
command: `${args.command.
|
|
506
|
+
command: `${args.command.slice(0, at)} ${DIAGNOSIS_TRACE_FLAG}${args.command.slice(at)}`,
|
|
463
507
|
change: `Added \`${DIAGNOSIS_TRACE_FLAG}\`: the suite's config records no trace for a failure` +
|
|
464
508
|
`${/trace:/.test(config) ? '' : ' (it sets none)'}, and this diagnosis run's failures are ` +
|
|
465
509
|
'what the platform reads the network log from. The proof run keeps the suite’s own settings.',
|