@molecule/api-ai-tools 1.12.0 → 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/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),
@@ -612,10 +613,17 @@ function isStatusOnlyStep(step) {
612
613
  * excludes by name ({@link DiagnosisRunContext}). A `--grep-invert` of exactly those is the
613
614
  * request's own scope, not a diagnosis, so it keeps the suite's retries on a deployed proof
614
615
  * (xr-molecule-flash-m170 +33:16).
615
- * @returns The command to run and one sentence per change made (empty when none), or `error` when
616
- * the command starts the suite more than once.
617
- */
618
- export function streamingBackgroundCommand(command, context) {
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) {
619
627
  // A command that runs no test is not a run to keep a growing log for: nothing about it is
620
628
  // rewritten (flash-m79's `--list` was read as a whole-suite run end to end, and got a ~4 KB
621
629
  // refusal to send itself again with `--max-failures=15`).
@@ -743,14 +751,26 @@ export function streamingBackgroundCommand(command, context) {
743
751
  // The last step's reporters: one printer, and it is `list`. Every branch below only ever moves
744
752
  // the command TO that shape — a reporter set is never left with two printers in it.
745
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;
746
762
  if (reporters) {
747
763
  const printing = reporters.values.filter((v) => PRINTING_REPORTERS.has(v));
748
764
  // Already exactly `list`: the shape, and nothing printing beside it. Left verbatim — the
749
765
  // executor's own spelling of the command is not rewritten for nothing.
750
- if (printing.length > 0 && !(printing.length === 1 && printing[0] === 'list')) {
766
+ const printerRewrite = printing.length > 0 && !(printing.length === 1 && printing[0] === 'list');
767
+ const addReport = reportWanted && !reporters.values.includes('json');
768
+ if (printerRewrite || addReport) {
751
769
  // The printing reporters become ONE `list`, in the place of the first of them; every other
752
770
  // reporter (a `json`/`html`/`junit` file the executor asked for, or one this build does not
753
- // 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.
754
774
  let placed = false;
755
775
  const kept = reporters.values
756
776
  .map((v) => {
@@ -762,6 +782,11 @@ export function streamingBackgroundCommand(command, context) {
762
782
  return 'list';
763
783
  })
764
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');
765
790
  // Only the last step is rewritten, and its offsets are the parse's own. The punctuation
766
791
  // after the step (`; }`, or the `)` attached to it) is kept verbatim.
767
792
  out =
@@ -771,26 +796,39 @@ export function streamingBackgroundCommand(command, context) {
771
796
  lastStep.slice(reporters.end) +
772
797
  rawLastStep.slice(lastStep.length) +
773
798
  out.slice(stepStart + rawLastStep.length);
774
- changes.push(
775
- // `list` was already there and a second printer sat beside it: that one is dropped. When
776
- // no `list` was named at all, the printer is SWITCHED to it (flash-m62's `line`).
777
- printing.includes('list')
778
- ? `Dropped \`${printing
779
- .filter((v) => v !== 'list')
780
- .join(',')}\` from your \`--reporter=${reporters.values.join(',')}\`: ` +
781
- 'two reporters that both print each failure write every test result into the log ' +
782
- 'twice — the whole log doubled for nothing (X1 flash-m83) — so this run keeps ' +
783
- '`list` alone.'
784
- : `Switched the test reporters to \`--reporter${reporters.separator}${kept.join(',')}\`: ` +
785
- '`list` writes each test result as it finishes and is the shape the platform reads ' +
786
- 'back from a background run, and it is the only printing reporter — two of them ' +
787
- 'write every result into the log twice (X1 flash-m83).');
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;
788
817
  }
789
818
  }
790
- else if (finalPlaywrightRun && addToRun('--reporter=list')) {
819
+ else if (finalPlaywrightRun &&
820
+ addToRun(reportWanted ? '--reporter=list,json' : '--reporter=list')) {
821
+ if (reportWanted)
822
+ report = reportPath;
791
823
  changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
792
824
  '(the config reporter may print nothing until the end).');
793
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
+ }
794
832
  // The run's failures, in the log as they fail (see PLAYWRIGHT_FAILURES_INLINE_ENV), on EVERY
795
833
  // background command that is not a no-test invocation — not only the ones whose text this
796
834
  // reader can place the runner in. The executor starts the suite through its own wrapper often
@@ -825,7 +863,58 @@ export function streamingBackgroundCommand(command, context) {
825
863
  if (addToRun('--retries=0'))
826
864
  changes.push(`Added \`--retries=0\`: ${why}.`);
827
865
  }
828
- 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
+ };
829
918
  }
830
919
  /**
831
920
  * How a shell command starts each runner whose whole-suite run saturates the
@@ -1248,6 +1337,10 @@ function lastSuiteRun(command) {
1248
1337
  * tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
1249
1338
  * that command back).
1250
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
+ *
1251
1344
  * @param command - The shell command.
1252
1345
  * @returns The rewritten command and the stages dropped.
1253
1346
  */
@@ -1281,15 +1374,40 @@ export function stripTrailingSuiteFilters(command) {
1281
1374
  // A stage inside a nested `sh -c` string has no offsets in this command: nothing can be cut.
1282
1375
  if (previous.end === null || stage.start === null || lastDropped.end === null)
1283
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;
1284
1402
  const pipe = stage.between.indexOf('|');
1285
1403
  if (pipe < 0)
1286
1404
  return { command, dropped: '' };
1287
1405
  const cut = previous.end + pipe;
1288
1406
  // The stages dropped are a middle slice when a statement follows, so the text after them is
1289
- // kept — `; echo done` still runs.
1407
+ // kept — `; echo done` still runs, and so does the punctuation closing the group they sat in.
1290
1408
  return {
1291
- command: `${command.slice(0, cut).trimEnd()}${command.slice(lastDropped.end)}`,
1292
- dropped: command.slice(cut, lastDropped.end).trim(),
1409
+ command: `${command.slice(0, cut).trimEnd()}${command.slice(dropEnd)}`,
1410
+ dropped: command.slice(cut, dropEnd).trim(),
1293
1411
  };
1294
1412
  }
1295
1413
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.12.0",
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.11"
38
+ "vitest": "4.1.12"
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",