@molecule/api-ai-tools 1.11.1 → 1.13.0
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 +460 -7
- package/dist/command-position.d.ts +33 -0
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/playwright-report.d.ts +210 -0
- package/dist/playwright-report.d.ts.map +1 -0
- package/dist/playwright-report.js +406 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +8 -0
- package/dist/server-command.d.ts +9 -3
- package/dist/server-command.d.ts.map +1 -1
- package/dist/server-command.js +39 -4
- package/dist/tool-catalog.d.ts +32 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +76 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +137 -8
- package/dist/trace-artifacts.d.ts +12 -0
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +1 -0
- package/dist/utilities.d.ts +78 -4
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +229 -38
- package/package.json +2 -2
package/dist/utilities.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { posix } from 'path';
|
|
8
|
-
import { commandSegments, executedCommands, locatedCommandSegments, statementArgumentEnd, withoutTimeoutWrapper, } from './command-position.js';
|
|
8
|
+
import { commandSegments, executedCommands, locatedCommandSegments, statementArgumentEnd, statementBoundary, withoutTimeoutWrapper, } from './command-position.js';
|
|
9
9
|
import { classifyRunFailure } from './failure-kind.js';
|
|
10
10
|
import { commandRunsNoTests } from './no-test-run.js';
|
|
11
|
+
import { PLAYWRIGHT_REPORT_ENV } from './playwright-report.js';
|
|
11
12
|
import { isDiagnosisRun, scopedRerunCommand, suiteCommandTarget, suiteRunInTheWay, withoutHeredocs, withSuiteConfig, } from './suite-overlap.js';
|
|
12
13
|
/**
|
|
13
14
|
* Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
|
|
@@ -163,8 +164,41 @@ export function redactSecrets(s) {
|
|
|
163
164
|
export function redactSecretsInCode(s) {
|
|
164
165
|
return s.replace(SECRET_ENV_ASSIGNMENT, '$1=[REDACTED]');
|
|
165
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The placeholder the output filter substitutes for a hidden value. Copying it into
|
|
169
|
+
* application configuration changes the value rather than recovering it. The write
|
|
170
|
+
* tools check for added occurrences ({@link addedRedactionMask}); intentional literals
|
|
171
|
+
* require the caller's explicit per-call declaration.
|
|
172
|
+
*
|
|
173
|
+
* Only the shapes a value can take count: quoted (`'[REDACTED]'`, `"[REDACTED]"`) or
|
|
174
|
+
* assigned (`=[REDACTED]`). Prose that mentions the token is ordinary text.
|
|
175
|
+
*/
|
|
176
|
+
const REDACTION_MASK = /'\[REDACTED\]'|"\[REDACTED\]"|=[ \t]*\[REDACTED\]/g;
|
|
177
|
+
/**
|
|
178
|
+
* The redaction mask `next` adds over `previous`, or null when it adds none.
|
|
179
|
+
*
|
|
180
|
+
* `previous` is the file's previous content for `write_file`, and the replacement's own
|
|
181
|
+
* `old_string` for `edit_file` — an edit that carries an existing mask through (a file that
|
|
182
|
+
* already had one, read back and rewritten) adds no occurrence. A new occurrence may
|
|
183
|
+
* be copied output or intentional source; the caller declares the latter explicitly.
|
|
184
|
+
*
|
|
185
|
+
* @param next - The content about to be written (or the replacement's `new_string`).
|
|
186
|
+
* @param previous - The content it replaces, already on disk (or the `old_string`).
|
|
187
|
+
* @returns The added mask as written (`'[REDACTED]'`, `"[REDACTED]"`, `=[REDACTED]`), or null.
|
|
188
|
+
*/
|
|
189
|
+
export function addedRedactionMask(next, previous) {
|
|
190
|
+
const before = previous.match(REDACTION_MASK)?.length ?? 0;
|
|
191
|
+
const after = next.match(REDACTION_MASK) ?? [];
|
|
192
|
+
if (after.length <= before)
|
|
193
|
+
return null;
|
|
194
|
+
return after[before] ?? after[0] ?? '[REDACTED]';
|
|
195
|
+
}
|
|
166
196
|
/** Source-file extensions a printing command's output is read as code for. */
|
|
167
197
|
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|css|scss|html|vue|svelte|md|sql|hurl)$/;
|
|
198
|
+
/** Programs whose output is file NAMES or counts, never another file's content. */
|
|
199
|
+
const INSPECTION_PROGRAMS = new Set(['find', 'ls', 'wc']);
|
|
200
|
+
/** `find` options that run a program or write a file — not read-only inspection. */
|
|
201
|
+
const FIND_NOT_INSPECTION = /^-(?:exec|execdir|ok|okdir|delete|fls|fprint|fprint0|fprintf)/;
|
|
168
202
|
/**
|
|
169
203
|
* A literal argument, not a shell expansion. The shared parser owns statement boundaries;
|
|
170
204
|
* this deliberately small word grammar only decides whether an argument can read other data.
|
|
@@ -253,9 +287,46 @@ function sourcePrintFiles(program, args) {
|
|
|
253
287
|
* @returns True when the command prints source files, with only literal headings and filters.
|
|
254
288
|
*/
|
|
255
289
|
export function printsOnlySource(command) {
|
|
290
|
+
return printsOnly(command, false);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Whether a shell command only INSPECTS the workspace WITHOUT printing a file's contents:
|
|
294
|
+
* it prints source files (a pure literal read, {@link printsOnlySource}), lists names
|
|
295
|
+
* (`ls`, `find`), or counts lines (`wc`) — joined by `;`, `&&` or `|`, with literal
|
|
296
|
+
* `echo`/`printf` headings and `cd`.
|
|
297
|
+
*
|
|
298
|
+
* Its output is code or names, not an env dump, so it gets the code-safe redaction
|
|
299
|
+
* ({@link redactSecretsInCode}) instead of the name-keyed env-dump passes. Without this,
|
|
300
|
+
* ordinary code values such as `tokenKey: 'jwtToken'` in a mixed source read and
|
|
301
|
+
* `ls | head` command would be masked by the environment-dump rules.
|
|
302
|
+
*
|
|
303
|
+
* A RECURSIVE SEARCH IS NOT INSPECTION (`grep -rn x .`, `rg x src/`): its output is the
|
|
304
|
+
* content of whichever files matched, including `.env` files and JSON secret dumps, so it
|
|
305
|
+
* keeps the full grade. A path with no extension is not evidence that it is a directory —
|
|
306
|
+
* `/dev/stdin` and an ordinary `secrets` file read the same way.
|
|
307
|
+
*
|
|
308
|
+
* Env dumps keep the full grade: `.env` anywhere in the command, `env`/`printenv`, a
|
|
309
|
+
* non-literal or redirecting form, and an interpreter one-liner (`node -e`, `python -c`)
|
|
310
|
+
* all return false.
|
|
311
|
+
*
|
|
312
|
+
* @param command - The shell command.
|
|
313
|
+
* @returns True when the command only reads or lists workspace content.
|
|
314
|
+
*/
|
|
315
|
+
export function isReadOnlyInspection(command) {
|
|
316
|
+
return printsOnly(command, true);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The one segment grammar both grades share: literal words only, no wrapper or group, no
|
|
320
|
+
* backgrounding, and every byte between segments a separator.
|
|
321
|
+
*
|
|
322
|
+
* @param command - The shell command.
|
|
323
|
+
* @param inspection - Also accept name/count listing (`ls`, `find`, `wc`).
|
|
324
|
+
* @returns True when every segment is allowed and at least one prints something.
|
|
325
|
+
*/
|
|
326
|
+
function printsOnly(command, inspection) {
|
|
256
327
|
if (/\.env\b/.test(command))
|
|
257
328
|
return false;
|
|
258
|
-
let
|
|
329
|
+
let prints = false;
|
|
259
330
|
let previousEnd = 0;
|
|
260
331
|
for (const located of locatedCommandSegments(command)) {
|
|
261
332
|
const { segment, start, end, programOffset, separator, terminator } = located;
|
|
@@ -302,6 +373,13 @@ export function printsOnlySource(command) {
|
|
|
302
373
|
return false;
|
|
303
374
|
}
|
|
304
375
|
}
|
|
376
|
+
else if (inspection && INSPECTION_PROGRAMS.has(program) && separator !== '|') {
|
|
377
|
+
// Names and counts only — nothing here can be another file's content. `find` is the
|
|
378
|
+
// exception that can run a program or write a file.
|
|
379
|
+
if (program === 'find' && args.some((arg) => FIND_NOT_INSPECTION.test(arg)))
|
|
380
|
+
return false;
|
|
381
|
+
prints = true;
|
|
382
|
+
}
|
|
305
383
|
else {
|
|
306
384
|
const filter = separator === '|';
|
|
307
385
|
if (!(filter ? /^(?:head|tail|sort|uniq|wc|cut|nl|grep)$/ : /^(?:cat|head|tail|nl|sed|grep|rg)$/).test(program)) {
|
|
@@ -319,11 +397,11 @@ export function printsOnlySource(command) {
|
|
|
319
397
|
!files.every((file) => !file.startsWith('-') && SOURCE_FILE_RE.test(file))) {
|
|
320
398
|
return false;
|
|
321
399
|
}
|
|
322
|
-
|
|
400
|
+
prints = true;
|
|
323
401
|
}
|
|
324
402
|
}
|
|
325
403
|
}
|
|
326
|
-
return
|
|
404
|
+
return prints && /^[\s;]*$/.test(command.slice(previousEnd));
|
|
327
405
|
}
|
|
328
406
|
/**
|
|
329
407
|
* Whether a path is an env file, for which {@link redactSecrets}' full env-dump
|
|
@@ -535,10 +613,17 @@ function isStatusOnlyStep(step) {
|
|
|
535
613
|
* excludes by name ({@link DiagnosisRunContext}). A `--grep-invert` of exactly those is the
|
|
536
614
|
* request's own scope, not a diagnosis, so it keeps the suite's retries on a deployed proof
|
|
537
615
|
* (xr-molecule-flash-m170 +33:16).
|
|
538
|
-
* @
|
|
539
|
-
*
|
|
540
|
-
|
|
541
|
-
|
|
616
|
+
* @param reportPath - Where this run should write Playwright's standard `json` report, when the
|
|
617
|
+
* caller keeps one ({@link ./playwright-report.js}). The reporter is added to the run's own
|
|
618
|
+
* reporter set, never in place of one, and its output goes to that file through
|
|
619
|
+
* `PLAYWRIGHT_JSON_OUTPUT_FILE` — so the log, the executors' own reporters and every verdict
|
|
620
|
+
* stay exactly as they are. Omitted, or a run whose reporter set is not this function's to place
|
|
621
|
+
* (already names `json`, sets the variable itself, or whose runner cannot be located), leaves
|
|
622
|
+
* the command untouched and the result's `reportPath` unset.
|
|
623
|
+
* @returns The command to run, one sentence per change made (empty when none), and the report path
|
|
624
|
+
* when one was actually wired — or `error` when the command starts the suite more than once.
|
|
625
|
+
*/
|
|
626
|
+
export function streamingBackgroundCommand(command, context, reportPath) {
|
|
542
627
|
// A command that runs no test is not a run to keep a growing log for: nothing about it is
|
|
543
628
|
// rewritten (flash-m79's `--list` was read as a whole-suite run end to end, and got a ~4 KB
|
|
544
629
|
// refusal to send itself again with `--max-failures=15`).
|
|
@@ -666,14 +751,26 @@ export function streamingBackgroundCommand(command, context) {
|
|
|
666
751
|
// The last step's reporters: one printer, and it is `list`. Every branch below only ever moves
|
|
667
752
|
// the command TO that shape — a reporter set is never left with two printers in it.
|
|
668
753
|
const reporters = finalPlaywrightRun ? lastReporterFlag(lastStep) : null;
|
|
754
|
+
// The standard machine report this run may carry (`playwright-report.ts`). It is never taken from
|
|
755
|
+
// the executor: a reporter set already naming `json`, or a command that sets
|
|
756
|
+
// `PLAYWRIGHT_JSON_OUTPUT_FILE` itself, is the executor's own artifact and is left untouched.
|
|
757
|
+
const reportWanted = reportPath !== undefined &&
|
|
758
|
+
finalPlaywrightRun &&
|
|
759
|
+
!/(?:^|[\s;])PLAYWRIGHT_JSON_OUTPUT_FILE=/.test(out) &&
|
|
760
|
+
!(reporters?.values.includes('json') ?? false);
|
|
761
|
+
let report = null;
|
|
669
762
|
if (reporters) {
|
|
670
763
|
const printing = reporters.values.filter((v) => PRINTING_REPORTERS.has(v));
|
|
671
764
|
// Already exactly `list`: the shape, and nothing printing beside it. Left verbatim — the
|
|
672
765
|
// executor's own spelling of the command is not rewritten for nothing.
|
|
673
|
-
|
|
766
|
+
const printerRewrite = printing.length > 0 && !(printing.length === 1 && printing[0] === 'list');
|
|
767
|
+
const addReport = reportWanted && !reporters.values.includes('json');
|
|
768
|
+
if (printerRewrite || addReport) {
|
|
674
769
|
// The printing reporters become ONE `list`, in the place of the first of them; every other
|
|
675
770
|
// reporter (a `json`/`html`/`junit` file the executor asked for, or one this build does not
|
|
676
|
-
// know) is left where it was.
|
|
771
|
+
// know) is left where it was. The machine report, when this run carries one, is appended to
|
|
772
|
+
// the same set: it writes the runner's own per-test identities and attachment paths to a
|
|
773
|
+
// file and prints nothing into the log.
|
|
677
774
|
let placed = false;
|
|
678
775
|
const kept = reporters.values
|
|
679
776
|
.map((v) => {
|
|
@@ -685,6 +782,11 @@ export function streamingBackgroundCommand(command, context) {
|
|
|
685
782
|
return 'list';
|
|
686
783
|
})
|
|
687
784
|
.filter((v) => v !== null);
|
|
785
|
+
// The sentence below is about the reporTERS this run prints with, so it reads the set as it
|
|
786
|
+
// was rewritten — the machine report is written to a file and is no part of that sentence.
|
|
787
|
+
const printers = kept.join(',');
|
|
788
|
+
if (addReport)
|
|
789
|
+
kept.push('json');
|
|
688
790
|
// Only the last step is rewritten, and its offsets are the parse's own. The punctuation
|
|
689
791
|
// after the step (`; }`, or the `)` attached to it) is kept verbatim.
|
|
690
792
|
out =
|
|
@@ -694,26 +796,39 @@ export function streamingBackgroundCommand(command, context) {
|
|
|
694
796
|
lastStep.slice(reporters.end) +
|
|
695
797
|
rawLastStep.slice(lastStep.length) +
|
|
696
798
|
out.slice(stepStart + rawLastStep.length);
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
799
|
+
if (printerRewrite) {
|
|
800
|
+
changes.push(
|
|
801
|
+
// `list` was already there and a second printer sat beside it: that one is dropped. When
|
|
802
|
+
// no `list` was named at all, the printer is SWITCHED to it (flash-m62's `line`).
|
|
803
|
+
printing.includes('list')
|
|
804
|
+
? `Dropped \`${printing
|
|
805
|
+
.filter((v) => v !== 'list')
|
|
806
|
+
.join(',')}\` from your \`--reporter=${reporters.values.join(',')}\`: ` +
|
|
807
|
+
'two reporters that both print each failure write every test result into the log ' +
|
|
808
|
+
'twice — the whole log doubled for nothing (X1 flash-m83) — so this run keeps ' +
|
|
809
|
+
'`list` alone.'
|
|
810
|
+
: `Switched the test reporters to \`--reporter${reporters.separator}${printers}\`: ` +
|
|
811
|
+
'`list` writes each test result as it finishes and is the shape the platform reads ' +
|
|
812
|
+
'back from a background run, and it is the only printing reporter — two of them ' +
|
|
813
|
+
'write every result into the log twice (X1 flash-m83).');
|
|
814
|
+
}
|
|
815
|
+
if (addReport)
|
|
816
|
+
report = reportPath;
|
|
711
817
|
}
|
|
712
818
|
}
|
|
713
|
-
else if (finalPlaywrightRun &&
|
|
819
|
+
else if (finalPlaywrightRun &&
|
|
820
|
+
addToRun(reportWanted ? '--reporter=list,json' : '--reporter=list')) {
|
|
821
|
+
if (reportWanted)
|
|
822
|
+
report = reportPath;
|
|
714
823
|
changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
|
|
715
824
|
'(the config reporter may print nothing until the end).');
|
|
716
825
|
}
|
|
826
|
+
// The report's own file option, set once at the head of the whole command — the json reporter
|
|
827
|
+
// reads it from the environment, resolved against the runner's directory, so an absolute path is
|
|
828
|
+
// used and the file lands beside the task's own log whatever `cd` the run opens with.
|
|
829
|
+
if (report !== null) {
|
|
830
|
+
out = `export ${PLAYWRIGHT_REPORT_ENV}=${shellQuote(report)}; ${out}`;
|
|
831
|
+
}
|
|
717
832
|
// The run's failures, in the log as they fail (see PLAYWRIGHT_FAILURES_INLINE_ENV), on EVERY
|
|
718
833
|
// background command that is not a no-test invocation — not only the ones whose text this
|
|
719
834
|
// reader can place the runner in. The executor starts the suite through its own wrapper often
|
|
@@ -748,7 +863,58 @@ export function streamingBackgroundCommand(command, context) {
|
|
|
748
863
|
if (addToRun('--retries=0'))
|
|
749
864
|
changes.push(`Added \`--retries=0\`: ${why}.`);
|
|
750
865
|
}
|
|
751
|
-
return { command: out, changes };
|
|
866
|
+
return { command: out, changes, ...(report === null ? {} : { reportPath: report }) };
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Put Playwright's standard `json` report on a FOREGROUND run that already names its own reporters.
|
|
870
|
+
*
|
|
871
|
+
* A background run is rewritten onto the platform's reporter anyway ({@link
|
|
872
|
+
* streamingBackgroundCommand}), so adding the machine report there costs nothing. A foreground run
|
|
873
|
+
* is not: its reporters are either the command's own flags or the suite's config, and replacing a
|
|
874
|
+
* config reporter would stop writing the artifact the executor configured. So this only ever
|
|
875
|
+
* APPENDS `json` to a `--reporter` set the command already wrote — the log keeps every printing
|
|
876
|
+
* reporter it had, the executor's file reporters keep their place, and the standard report goes to
|
|
877
|
+
* {@link PLAYWRIGHT_REPORT_ENV}'s file.
|
|
878
|
+
*
|
|
879
|
+
* A command that names no reporter at all is left exactly as written: its reporters are the suite
|
|
880
|
+
* config's, and this tool does not rewrite those. A command that already names `json`, or sets
|
|
881
|
+
* `PLAYWRIGHT_JSON_OUTPUT_FILE` itself, already has a report of its own — a path the executor chose
|
|
882
|
+
* is never overwritten.
|
|
883
|
+
*
|
|
884
|
+
* @param command - The command the executor sent, after every other rewrite.
|
|
885
|
+
* @param reportPath - The file this run's machine report should be written to.
|
|
886
|
+
* @returns The command to run and the report path when one was wired; `reportPath` is null when the
|
|
887
|
+
* run was left untouched and no report may be expected.
|
|
888
|
+
*/
|
|
889
|
+
export function withPlaywrightRunReport(command, reportPath) {
|
|
890
|
+
const unchanged = { command, reportPath: null };
|
|
891
|
+
if (reportPath === '')
|
|
892
|
+
return unchanged;
|
|
893
|
+
if (!isPlaywrightRunCommand(command))
|
|
894
|
+
return unchanged;
|
|
895
|
+
if (/(?:^|[\s;])PLAYWRIGHT_JSON_OUTPUT_FILE=/.test(command))
|
|
896
|
+
return unchanged;
|
|
897
|
+
const run = lastSuiteRun(command);
|
|
898
|
+
const runner = run ? run.located[run.at] : undefined;
|
|
899
|
+
if (!runner || runner.start === null || runner.end === null)
|
|
900
|
+
return unchanged;
|
|
901
|
+
const at = statementArgumentEnd(command, runner.start, runner.end);
|
|
902
|
+
if (at === null)
|
|
903
|
+
return unchanged;
|
|
904
|
+
// The reporter flag the RUN's own statement names — not one an argument of another program
|
|
905
|
+
// carries (`grep --reporter x file`): the slice starts at the runner's own program word.
|
|
906
|
+
const flag = lastReporterFlag(command.slice(runner.start, at));
|
|
907
|
+
if (flag === null || flag.values.includes('json'))
|
|
908
|
+
return unchanged;
|
|
909
|
+
const start = runner.start + flag.start;
|
|
910
|
+
const end = runner.start + flag.end;
|
|
911
|
+
const rewritten = command.slice(0, start) +
|
|
912
|
+
`--reporter${flag.separator}${[...flag.values, 'json'].join(',')}` +
|
|
913
|
+
command.slice(end);
|
|
914
|
+
return {
|
|
915
|
+
command: `export ${PLAYWRIGHT_REPORT_ENV}=${shellQuote(reportPath)}; ${rewritten}`,
|
|
916
|
+
reportPath,
|
|
917
|
+
};
|
|
752
918
|
}
|
|
753
919
|
/**
|
|
754
920
|
* How a shell command starts each runner whose whole-suite run saturates the
|
|
@@ -1171,6 +1337,10 @@ function lastSuiteRun(command) {
|
|
|
1171
1337
|
* tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
|
|
1172
1338
|
* that command back).
|
|
1173
1339
|
*
|
|
1340
|
+
* The command around the dropped stages is left as written: a group they sit in keeps its closer
|
|
1341
|
+
* (`( … | tail -25)` → `( … )`, never `( …` — xr-molecule-flash-m418 +23:31), and a stage the cut
|
|
1342
|
+
* would leave a closer dangling inside is not dropped at all.
|
|
1343
|
+
*
|
|
1174
1344
|
* @param command - The shell command.
|
|
1175
1345
|
* @returns The rewritten command and the stages dropped.
|
|
1176
1346
|
*/
|
|
@@ -1204,15 +1374,40 @@ export function stripTrailingSuiteFilters(command) {
|
|
|
1204
1374
|
// A stage inside a nested `sh -c` string has no offsets in this command: nothing can be cut.
|
|
1205
1375
|
if (previous.end === null || stage.start === null || lastDropped.end === null)
|
|
1206
1376
|
return { command, dropped: '' };
|
|
1377
|
+
// Every stage dropped must be provably whole inside the command's own structure. A group
|
|
1378
|
+
// closer attached to one of them closes a group the pipeline below still runs in
|
|
1379
|
+
// (`( … | tail -5) | cat`), and a stage that OPENS a group it does not close (`| { cat; }`) has
|
|
1380
|
+
// its closer in a later statement — either cut would leave the punctuation dangling. Only the
|
|
1381
|
+
// last stage may end on an enclosing closer, and it is kept ({@link statementBoundary}).
|
|
1382
|
+
const boundaries = [];
|
|
1383
|
+
for (let i = first; i <= end; i += 1) {
|
|
1384
|
+
const droppedStage = located[i];
|
|
1385
|
+
if (!droppedStage || droppedStage.end === null)
|
|
1386
|
+
return { command, dropped: '' };
|
|
1387
|
+
const boundary = statementBoundary(command, droppedStage.start, droppedStage.end);
|
|
1388
|
+
if (boundary === null || boundary.leavesGroupOpen)
|
|
1389
|
+
return { command, dropped: '' };
|
|
1390
|
+
if (i < end && boundary.textEnd !== droppedStage.end)
|
|
1391
|
+
return { command, dropped: '' };
|
|
1392
|
+
boundaries.push(boundary);
|
|
1393
|
+
}
|
|
1394
|
+
const lastBoundary = boundaries[boundaries.length - 1];
|
|
1395
|
+
if (lastBoundary === undefined)
|
|
1396
|
+
return { command, dropped: '' };
|
|
1397
|
+
// Where the dropped stages' own text ends. The tokenizer attaches the closer of an enclosing
|
|
1398
|
+
// group to the stage's last word (`tail -25)`), so cutting at {@link LocatedSegment.end} deleted
|
|
1399
|
+
// the `)` and left `( … 2>&1` — a command no shell parses, which the background runner rejected
|
|
1400
|
+
// with `end of file unexpected` (xr-molecule-flash-m418 +23:31).
|
|
1401
|
+
const dropEnd = lastBoundary.textEnd;
|
|
1207
1402
|
const pipe = stage.between.indexOf('|');
|
|
1208
1403
|
if (pipe < 0)
|
|
1209
1404
|
return { command, dropped: '' };
|
|
1210
1405
|
const cut = previous.end + pipe;
|
|
1211
1406
|
// The stages dropped are a middle slice when a statement follows, so the text after them is
|
|
1212
|
-
// kept — `; echo done` still runs.
|
|
1407
|
+
// kept — `; echo done` still runs, and so does the punctuation closing the group they sat in.
|
|
1213
1408
|
return {
|
|
1214
|
-
command: `${command.slice(0, cut).trimEnd()}${command.slice(
|
|
1215
|
-
dropped: command.slice(cut,
|
|
1409
|
+
command: `${command.slice(0, cut).trimEnd()}${command.slice(dropEnd)}`,
|
|
1410
|
+
dropped: command.slice(cut, dropEnd).trim(),
|
|
1216
1411
|
};
|
|
1217
1412
|
}
|
|
1218
1413
|
/**
|
|
@@ -3980,11 +4175,8 @@ export function repeatingFailureNote(found, o) {
|
|
|
3980
4175
|
`repeats this one cause. Stop it and fix that cause first: \`exec_command: ${o.stop}\`, ` +
|
|
3981
4176
|
`then run the whole suite again. It is still going (${where}).${reproduce}`);
|
|
3982
4177
|
}
|
|
3983
|
-
//
|
|
3984
|
-
//
|
|
3985
|
-
// not. What it does NOT carry is the "Stop it if every test fails this way" clause — that one
|
|
3986
|
-
// belongs to a PROVEN cause the run is still walking (the sentence below), never to a lead:
|
|
3987
|
-
// xr-molecule-flash-m99's executor read it on a shared duration and voided the run.
|
|
4178
|
+
// The same predicate controls both an early return and stop advice. Without
|
|
4179
|
+
// that verdict, neither a repeated error nor a shared duration asks for a stop.
|
|
3988
4180
|
const keepGoing = `Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
|
|
3989
4181
|
`this one, and a run stopped at its first repeating cause costs a second whole run to find ` +
|
|
3990
4182
|
`them. It is still going (${where}).`;
|
|
@@ -3992,10 +4184,9 @@ export function repeatingFailureNote(found, o) {
|
|
|
3992
4184
|
return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
|
|
3993
4185
|
`so one cause is repeating:\n${rows}${more}\n` +
|
|
3994
4186
|
(o.editsInvalidateRun
|
|
3995
|
-
? `Work out the fix now: apply it when the run ends, not while it walks
|
|
3996
|
-
: `Start fixing it now, while the run keeps walking
|
|
3997
|
-
|
|
3998
|
-
`out on every test of every file so far).${reproduce} ${keepGoing}${thenRun}`);
|
|
4187
|
+
? `Work out the fix now: apply it when the run ends, not while it walks.`
|
|
4188
|
+
: `Start fixing it now, while the run keeps walking.`) +
|
|
4189
|
+
`${reproduce} ${keepGoing}${thenRun}`);
|
|
3999
4190
|
return (`${found.count} different tests in this run each failed after ~${found.seconds}s — the same ` +
|
|
4000
4191
|
`duration to within half a second. A shared duration is a lead, not a cause: a suite whose ` +
|
|
4001
4192
|
`config sets a timeout spends that timeout on every failure that waits for anything, so ` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-ai-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@types/node": "26.1.2",
|
|
36
36
|
"esbuild": "0.28.2",
|
|
37
37
|
"typescript": "6.0.3",
|
|
38
|
-
"vitest": "4.1.
|
|
38
|
+
"vitest": "4.1.12"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|