@molecule/api-ai-tools 1.8.2 → 1.9.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/README.md +413 -31
- package/dist/command-position.d.ts +26 -2
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +221 -24
- package/dist/failure-kind.d.ts +66 -0
- package/dist/failure-kind.d.ts.map +1 -0
- package/dist/failure-kind.js +134 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/sleep-peek.d.ts +51 -0
- package/dist/sleep-peek.d.ts.map +1 -0
- package/dist/sleep-peek.js +76 -0
- package/dist/suite-overlap.d.ts +35 -1
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +57 -2
- package/dist/tools.d.ts +56 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +285 -48
- package/dist/trace-artifacts.d.ts +77 -7
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +344 -47
- package/dist/utilities.d.ts +63 -13
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +277 -64
- package/package.json +1 -1
|
@@ -21,8 +21,14 @@
|
|
|
21
21
|
* Not command position, and therefore not an invocation: a runner named as an argument
|
|
22
22
|
* (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
|
|
23
23
|
* `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
|
|
24
|
-
* WRITES is
|
|
25
|
-
*
|
|
24
|
+
* WRITES is not a command it runs, so the bodies are blanked to spaces before anything is split
|
|
25
|
+
* ({@link blankHeredocBodies}); offsets stay exact, because the blanking preserves every position.
|
|
26
|
+
*
|
|
27
|
+
* The parser also answers WHERE each statement sits ({@link locatedCommandSegments}): `start`,
|
|
28
|
+
* `end`, `programOffset` and `assignmentOffset` are the offsets a caller splices at. They are
|
|
29
|
+
* token positions from this walk, never a search for re-joined text — the words are joined back
|
|
30
|
+
* with single spaces for matching, so `command.indexOf(segment.text)` fails on irregular spacing,
|
|
31
|
+
* on grouping braces, and on anything a heredoc body shifts (xr-molecule-flash-m105 +08:12).
|
|
26
32
|
*
|
|
27
33
|
* @module
|
|
28
34
|
*/
|
|
@@ -90,6 +96,24 @@ export interface LocatedCommandSegment {
|
|
|
90
96
|
* inside a nested `sh -c` string, for the same reason as {@link start}.
|
|
91
97
|
*/
|
|
92
98
|
assignmentOffset: number | null;
|
|
99
|
+
/**
|
|
100
|
+
* The offset in the command one past the statement's last word — where an option of this
|
|
101
|
+
* statement's program belongs, before whatever closes it. Null inside a nested `sh -c` string.
|
|
102
|
+
*/
|
|
103
|
+
end: number | null;
|
|
104
|
+
/**
|
|
105
|
+
* The offset in the command of the word in command position — the program, once every wrapper
|
|
106
|
+
* in front of it is taken away (`npx --prefix /app <here>playwright test`). Null inside a
|
|
107
|
+
* nested `sh -c` string, and for a statement that only sets variables.
|
|
108
|
+
*/
|
|
109
|
+
programOffset: number | null;
|
|
110
|
+
/**
|
|
111
|
+
* The separator the shell split at immediately before this statement — `|`, `||`, `&&`, `;`,
|
|
112
|
+
* a newline or `&` — or '' when it is the first statement of the command. It is the parse's own
|
|
113
|
+
* answer, so it is known even where the offsets are not, and a caller deciding whether two
|
|
114
|
+
* statements are one pipeline (`|`, never `;`) asks it instead of reading the text between them.
|
|
115
|
+
*/
|
|
116
|
+
separator: string;
|
|
93
117
|
}
|
|
94
118
|
/**
|
|
95
119
|
* Every statement of a command, with its own assignments and where it sits — assignment-only
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AA2MD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AA+cD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AA4ED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
|
package/dist/command-position.js
CHANGED
|
@@ -21,8 +21,14 @@
|
|
|
21
21
|
* Not command position, and therefore not an invocation: a runner named as an argument
|
|
22
22
|
* (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
|
|
23
23
|
* `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
|
|
24
|
-
* WRITES is
|
|
25
|
-
*
|
|
24
|
+
* WRITES is not a command it runs, so the bodies are blanked to spaces before anything is split
|
|
25
|
+
* ({@link blankHeredocBodies}); offsets stay exact, because the blanking preserves every position.
|
|
26
|
+
*
|
|
27
|
+
* The parser also answers WHERE each statement sits ({@link locatedCommandSegments}): `start`,
|
|
28
|
+
* `end`, `programOffset` and `assignmentOffset` are the offsets a caller splices at. They are
|
|
29
|
+
* token positions from this walk, never a search for re-joined text — the words are joined back
|
|
30
|
+
* with single spaces for matching, so `command.indexOf(segment.text)` fails on irregular spacing,
|
|
31
|
+
* on grouping braces, and on anything a heredoc body shifts (xr-molecule-flash-m105 +08:12).
|
|
26
32
|
*
|
|
27
33
|
* @module
|
|
28
34
|
*/
|
|
@@ -380,6 +386,123 @@ function commandStart(words) {
|
|
|
380
386
|
}
|
|
381
387
|
return { at, nested: null, timeoutSeconds };
|
|
382
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* A command with every heredoc BODY blanked to spaces, so its length and every offset in it are
|
|
391
|
+
* unchanged.
|
|
392
|
+
*
|
|
393
|
+
* A heredoc body is data the command writes, not a command it runs: `cat > playwright.config.ts
|
|
394
|
+
* <<'EOF' … import { defineConfig, devices } from '@playwright/test' … EOF` writes a file and runs
|
|
395
|
+
* `cat`. Read as shell, though, every line of the body is a statement — and the words joined back
|
|
396
|
+
* for matching are not the words written, so a caller that placed a statement by searching for
|
|
397
|
+
* them found nothing, in a command whose real statements sat all along. On X1
|
|
398
|
+
* xr-molecule-flash-m105 (+08:12) the body's `import { defineConfig, devices } …` alone made
|
|
399
|
+
* `suiteRunPipedAfterRunner` answer "piped" for a run with no pipe left, and the run's exit code
|
|
400
|
+
* was dropped for the rest of the session.
|
|
401
|
+
*
|
|
402
|
+
* The body runs from the line after the operator to the line whose whole content is the delimiter
|
|
403
|
+
* (leading TABS stripped for `<<-`, as `man bash` defines it); the terminator line is blanked too,
|
|
404
|
+
* and its newline kept, so the statements around it still split at the same places. Newlines
|
|
405
|
+
* inside the body are kept for the same reason. `<<<` is a herestring, not a heredoc, and a `<<`
|
|
406
|
+
* inside quotes or a comment opens nothing.
|
|
407
|
+
*
|
|
408
|
+
* @param command - The shell command.
|
|
409
|
+
* @returns The command with the bodies blanked to spaces, the same length as the input.
|
|
410
|
+
*/
|
|
411
|
+
function blankHeredocBodies(command) {
|
|
412
|
+
if (!command.includes('<<'))
|
|
413
|
+
return command;
|
|
414
|
+
const chars = command.split('');
|
|
415
|
+
/** Overwrite a range with spaces, keeping its newlines so statements still split there. */
|
|
416
|
+
const blank = (from, to) => {
|
|
417
|
+
for (let i = from; i < to; i++)
|
|
418
|
+
if (chars[i] !== '\n')
|
|
419
|
+
chars[i] = ' ';
|
|
420
|
+
};
|
|
421
|
+
let lineStart = 0;
|
|
422
|
+
while (lineStart < command.length) {
|
|
423
|
+
const lineEndRaw = command.indexOf('\n', lineStart);
|
|
424
|
+
const lineEnd = lineEndRaw < 0 ? command.length : lineEndRaw;
|
|
425
|
+
const openers = [];
|
|
426
|
+
let quote = null;
|
|
427
|
+
for (let i = lineStart; i < lineEnd; i++) {
|
|
428
|
+
const char = command[i] ?? '';
|
|
429
|
+
if (quote !== null) {
|
|
430
|
+
if (char === quote)
|
|
431
|
+
quote = null;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (char === '"' || char === "'") {
|
|
435
|
+
quote = char;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// A `#` that starts a word opens a comment: the rest of the line is not shell words.
|
|
439
|
+
if (char === '#' && (i === lineStart || /\s/.test(command[i - 1] ?? '')))
|
|
440
|
+
break;
|
|
441
|
+
if (char !== '<' || command[i + 1] !== '<')
|
|
442
|
+
continue;
|
|
443
|
+
// `<<<` is a herestring: it opens no body.
|
|
444
|
+
if (command[i + 2] === '<') {
|
|
445
|
+
i += 2;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
let at = i + 2;
|
|
449
|
+
const stripTabs = command[at] === '-';
|
|
450
|
+
if (stripTabs)
|
|
451
|
+
at += 1;
|
|
452
|
+
while (command[at] === ' ' || command[at] === '\t')
|
|
453
|
+
at += 1;
|
|
454
|
+
let delimiter;
|
|
455
|
+
const opener = command[at];
|
|
456
|
+
if (opener === '"' || opener === "'") {
|
|
457
|
+
const close = command.indexOf(opener, at + 1);
|
|
458
|
+
const wordEnd = close < 0 || close > lineEnd ? lineEnd : close;
|
|
459
|
+
delimiter = command.slice(at + 1, wordEnd);
|
|
460
|
+
at = wordEnd + 1;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
let wordEnd = at;
|
|
464
|
+
while (wordEnd < lineEnd && !/\s/.test(command[wordEnd] ?? ''))
|
|
465
|
+
wordEnd += 1;
|
|
466
|
+
delimiter = command.slice(at, wordEnd);
|
|
467
|
+
at = wordEnd;
|
|
468
|
+
}
|
|
469
|
+
if (delimiter !== '')
|
|
470
|
+
openers.push({ delimiter, stripTabs });
|
|
471
|
+
i = at - 1;
|
|
472
|
+
}
|
|
473
|
+
if (openers.length === 0) {
|
|
474
|
+
if (lineEndRaw < 0)
|
|
475
|
+
break;
|
|
476
|
+
lineStart = lineEnd + 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
// The bodies follow the line that opened them, in the order the operators were written.
|
|
480
|
+
let cursor = lineEndRaw < 0 ? command.length : lineEnd + 1;
|
|
481
|
+
for (const heredoc of openers) {
|
|
482
|
+
let bodyEnd = command.length;
|
|
483
|
+
let after = command.length;
|
|
484
|
+
let from = cursor;
|
|
485
|
+
while (from < command.length) {
|
|
486
|
+
const nl = command.indexOf('\n', from);
|
|
487
|
+
const line = command.slice(from, nl < 0 ? command.length : nl);
|
|
488
|
+
const compared = heredoc.stripTabs ? line.replace(/^\t+/, '') : line;
|
|
489
|
+
if (compared === heredoc.delimiter) {
|
|
490
|
+
bodyEnd = from;
|
|
491
|
+
after = nl < 0 ? command.length : nl + 1;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
if (nl < 0)
|
|
495
|
+
break;
|
|
496
|
+
from = nl + 1;
|
|
497
|
+
}
|
|
498
|
+
blank(cursor, bodyEnd);
|
|
499
|
+
blank(bodyEnd, after);
|
|
500
|
+
cursor = after;
|
|
501
|
+
}
|
|
502
|
+
lineStart = cursor;
|
|
503
|
+
}
|
|
504
|
+
return chars.join('');
|
|
505
|
+
}
|
|
383
506
|
/**
|
|
384
507
|
* A command's statements and pipeline stages, split where the shell splits them — inside quotes
|
|
385
508
|
* and inside a command substitution, a `|`, `;` or `&&` belongs to that command, not to this one.
|
|
@@ -400,19 +523,21 @@ function commandStart(words) {
|
|
|
400
523
|
* no runner, and every run of it reads as nothing).
|
|
401
524
|
*
|
|
402
525
|
* @param command - The shell command.
|
|
403
|
-
* @returns Each
|
|
404
|
-
* order, with empty ones
|
|
526
|
+
* @returns Each statement's text, the offset its first character sits at, and the separator that
|
|
527
|
+
* ended it (`|`, `&&`, `;`, a newline, … — '' after the last one), in order, with empty ones
|
|
528
|
+
* dropped.
|
|
405
529
|
*/
|
|
406
530
|
function splitSegments(command) {
|
|
531
|
+
const source = blankHeredocBodies(command);
|
|
407
532
|
const parts = [];
|
|
408
533
|
let buffer = '';
|
|
409
534
|
/** The offset in the command of the current buffer's first character. */
|
|
410
535
|
let bufferStart = 0;
|
|
411
536
|
/** The nestings this position sits inside, outermost first. */
|
|
412
537
|
const stack = [];
|
|
413
|
-
for (let i = 0; i <
|
|
414
|
-
const char =
|
|
415
|
-
const next =
|
|
538
|
+
for (let i = 0; i < source.length; i++) {
|
|
539
|
+
const char = source[i] ?? '';
|
|
540
|
+
const next = source[i + 1];
|
|
416
541
|
const top = stack[stack.length - 1];
|
|
417
542
|
// A single-quoted string expands nothing at all: no escape, no substitution, and no quote
|
|
418
543
|
// but its own ends it.
|
|
@@ -469,33 +594,46 @@ function splitSegments(command) {
|
|
|
469
594
|
continue;
|
|
470
595
|
}
|
|
471
596
|
if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
|
|
472
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
597
|
+
parts.push({ text: buffer, start: bufferStart, after: char + next });
|
|
473
598
|
buffer = '';
|
|
474
599
|
i += 1;
|
|
475
600
|
bufferStart = i + 1;
|
|
476
601
|
continue;
|
|
477
602
|
}
|
|
478
603
|
if (char === ';' || char === '|' || char === '\n') {
|
|
479
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
604
|
+
parts.push({ text: buffer, start: bufferStart, after: char });
|
|
480
605
|
buffer = '';
|
|
481
606
|
bufferStart = i + 1;
|
|
482
607
|
continue;
|
|
483
608
|
}
|
|
484
|
-
if (char === '&' &&
|
|
485
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
609
|
+
if (char === '&' && source[i - 1] !== '>' && next !== '>') {
|
|
610
|
+
parts.push({ text: buffer, start: bufferStart, after: '&' });
|
|
486
611
|
buffer = '';
|
|
487
612
|
bufferStart = i + 1;
|
|
488
613
|
continue;
|
|
489
614
|
}
|
|
490
615
|
buffer += char;
|
|
491
616
|
}
|
|
492
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
617
|
+
parts.push({ text: buffer, start: bufferStart, after: '' });
|
|
618
|
+
const out = [];
|
|
619
|
+
// The separator of the last part, kept across the empty ones so a statement's own separator
|
|
620
|
+
// survives the blank lines around it (`a &&\n b` and a heredoc's blanked body alike).
|
|
621
|
+
let lastAfter = null;
|
|
622
|
+
for (const part of parts) {
|
|
623
|
+
const text = part.text.trim();
|
|
624
|
+
if (text === '') {
|
|
625
|
+
lastAfter = part.after;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
out.push({
|
|
629
|
+
text,
|
|
630
|
+
start: part.start + (part.text.length - part.text.trimStart().length),
|
|
631
|
+
after: part.after,
|
|
632
|
+
before: lastAfter ?? '',
|
|
633
|
+
});
|
|
634
|
+
lastAfter = part.after;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
499
637
|
}
|
|
500
638
|
/**
|
|
501
639
|
* One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
|
|
@@ -559,6 +697,42 @@ function leadingAssignments(text) {
|
|
|
559
697
|
export function commandSegments(command) {
|
|
560
698
|
return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
|
|
561
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Where a statement's word sits in its own raw text, as {@link segmentWords} counts the words.
|
|
702
|
+
*
|
|
703
|
+
* The words are matched for READING (quotes whole, grouping braces dropped, a leading `(` taken
|
|
704
|
+
* off the first word); a caller that writes into the statement needs the offset it was written
|
|
705
|
+
* at. Both come from this one walk, so the two can never disagree about which word is which.
|
|
706
|
+
*
|
|
707
|
+
* @param text - One statement's text.
|
|
708
|
+
* @param index - The word's index in {@link segmentWords}'s list.
|
|
709
|
+
* @returns The word's offset in `text`, or null when there is no such word.
|
|
710
|
+
*/
|
|
711
|
+
function wordOffset(text, index) {
|
|
712
|
+
const placement = new RegExp(WORD.source, 'g');
|
|
713
|
+
let kept = 0;
|
|
714
|
+
let first = true;
|
|
715
|
+
for (let match = placement.exec(text); match !== null; match = placement.exec(text)) {
|
|
716
|
+
if (GROUPING.has(match[0]))
|
|
717
|
+
continue;
|
|
718
|
+
if (first) {
|
|
719
|
+
first = false;
|
|
720
|
+
const bare = match[0].replace(/^[({]+/, '');
|
|
721
|
+
// `segmentWords` shifts a first word that was nothing but grouping away, so the NEXT word
|
|
722
|
+
// is not stripped and is counted from zero.
|
|
723
|
+
if (bare === '')
|
|
724
|
+
continue;
|
|
725
|
+
if (kept === index)
|
|
726
|
+
return match.index + (match[0].length - bare.length);
|
|
727
|
+
kept += 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (kept === index)
|
|
731
|
+
return match.index;
|
|
732
|
+
kept += 1;
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
562
736
|
/**
|
|
563
737
|
* Every statement of a command, with its own assignments and where it sits — assignment-only
|
|
564
738
|
* statements included, because the variables those persist are exactly what a later statement of
|
|
@@ -587,9 +761,31 @@ function locatedSegmentsOf(command, depth) {
|
|
|
587
761
|
const words = segmentWords(part.text);
|
|
588
762
|
const start = commandStart(words);
|
|
589
763
|
const leading = leadingAssignments(part.text);
|
|
764
|
+
/** Every offset in this statement is an offset in the command it was written in. */
|
|
765
|
+
const placed = {
|
|
766
|
+
start: part.start,
|
|
767
|
+
end: part.start + part.text.length,
|
|
768
|
+
assignmentOffset: part.start + leading.end,
|
|
769
|
+
programOffset: words[start.at] === undefined ? null : part.start + (wordOffset(part.text, start.at) ?? 0),
|
|
770
|
+
};
|
|
590
771
|
if (start.nested !== null) {
|
|
591
|
-
|
|
592
|
-
|
|
772
|
+
const inner = locatedSegmentsOf(start.nested, depth + 1);
|
|
773
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
774
|
+
const entry = inner[i];
|
|
775
|
+
if (!entry)
|
|
776
|
+
continue;
|
|
777
|
+
out.push({
|
|
778
|
+
...entry,
|
|
779
|
+
// A nested statement's positions are positions in the nested string, not in this
|
|
780
|
+
// command. The FIRST one keeps this statement's own separator: the shell reads the
|
|
781
|
+
// nested string where this statement stands.
|
|
782
|
+
start: null,
|
|
783
|
+
end: null,
|
|
784
|
+
assignmentOffset: null,
|
|
785
|
+
programOffset: null,
|
|
786
|
+
separator: i === 0 ? part.before : entry.separator,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
593
789
|
continue;
|
|
594
790
|
}
|
|
595
791
|
const word = words[start.at];
|
|
@@ -600,8 +796,9 @@ function locatedSegmentsOf(command, depth) {
|
|
|
600
796
|
out.push({
|
|
601
797
|
segment: null,
|
|
602
798
|
assignments: leading.words,
|
|
603
|
-
|
|
604
|
-
|
|
799
|
+
...placed,
|
|
800
|
+
programOffset: null,
|
|
801
|
+
separator: part.before,
|
|
605
802
|
});
|
|
606
803
|
continue;
|
|
607
804
|
}
|
|
@@ -615,8 +812,8 @@ function locatedSegmentsOf(command, depth) {
|
|
|
615
812
|
timeoutSeconds: start.timeoutSeconds,
|
|
616
813
|
},
|
|
617
814
|
assignments: leading.words,
|
|
618
|
-
|
|
619
|
-
|
|
815
|
+
...placed,
|
|
816
|
+
separator: part.before,
|
|
620
817
|
});
|
|
621
818
|
}
|
|
622
819
|
return out;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failing request actually hit, read off the runner's own output.
|
|
3
|
+
*
|
|
4
|
+
* Three layers fail in three different ways, and only one of them is the app's code:
|
|
5
|
+
*
|
|
6
|
+
* - **connection** — the request never got an answer at all: the transport timed out, was
|
|
7
|
+
* refused, reset, or could not resolve the name or negotiate TLS. Nothing about a handler,
|
|
8
|
+
* a header or CORS was measured, so a re-run is the only thing that tells a cold start (a
|
|
9
|
+
* machine deployed minutes ago whose boot finishes after the URL is live) from a real outage.
|
|
10
|
+
* - **status** — a response arrived and the HTTP status disagreed with what the suite asserts.
|
|
11
|
+
* - **assertion** — a response arrived and an assertion over its body failed.
|
|
12
|
+
*
|
|
13
|
+
* X0 xr-molecule-flash-m110 (+18:42 → +20:09) is the case this exists for: one Hurl file on the
|
|
14
|
+
* freshly deployed URL failed with `(28) Operation timed out … 0 bytes received` while the other
|
|
15
|
+
* twelve passed and 138 requests executed, and every note on the result read it as an assertion.
|
|
16
|
+
* The executor rewrote a CORS middleware, bisected a second server and latency-compared the two,
|
|
17
|
+
* then re-ran the same file — which passed in 3 s. A single re-run was the whole fix, and the one
|
|
18
|
+
* fact that would have said so is the failure's LAYER.
|
|
19
|
+
*
|
|
20
|
+
* ONE classifier, used by the causes note ({@link ./utilities.ts} `failureCausesNote`) and by the
|
|
21
|
+
* platform's own proof result (`molecule-dev` `suite-proof.ts`), so the two can never disagree
|
|
22
|
+
* about whether a failure was the app's to fix.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The layer a failure came from: the transport (no answer at all), the response's status, or an
|
|
28
|
+
* assertion over a response that arrived.
|
|
29
|
+
*/
|
|
30
|
+
export type RunFailureKind = 'connection' | 'status' | 'assertion';
|
|
31
|
+
/** What the transport did when a request never got an answer. */
|
|
32
|
+
export type ConnectionFailureKind = 'timeout' | 'refused' | 'reset' | 'dns' | 'tls' | 'empty';
|
|
33
|
+
/** One failure a runner printed, classified by the layer that failed. */
|
|
34
|
+
export interface RunFailureClass {
|
|
35
|
+
/** The layer that failed. */
|
|
36
|
+
kind: RunFailureKind;
|
|
37
|
+
/** For a `connection` failure, what the transport did. */
|
|
38
|
+
connection?: ConnectionFailureKind;
|
|
39
|
+
/** How long the request waited before the transport gave up, in seconds, when printed. */
|
|
40
|
+
seconds?: number;
|
|
41
|
+
/** The `file:line` the runner printed for the failure, when it printed one. */
|
|
42
|
+
at?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Classify ONE failure's text — a Hurl `error:` block, or a Playwright failure message/body.
|
|
46
|
+
*
|
|
47
|
+
* The transport is read first: a block that carries a connection error AND an assertion is a
|
|
48
|
+
* request whose assertion never ran, so it is a connection failure. Only a block that carries no
|
|
49
|
+
* failure signature at all answers null — unknown is never a connection failure, because a
|
|
50
|
+
* misread transport hides a real app failure behind "just re-run it".
|
|
51
|
+
*
|
|
52
|
+
* @param text - One failure's text, as the runner printed it.
|
|
53
|
+
* @returns The classification, or null when the text carries no failure this reads.
|
|
54
|
+
*/
|
|
55
|
+
export declare function classifyRunFailure(text: string): RunFailureClass | null;
|
|
56
|
+
/**
|
|
57
|
+
* Whether every one of the classified failures is a connection failure — the only shape in which
|
|
58
|
+
* no response the app produced was measured.
|
|
59
|
+
*
|
|
60
|
+
* @param failures - The run's classifications, in printed order.
|
|
61
|
+
* @returns True when there is at least one and none of them is the app's.
|
|
62
|
+
*/
|
|
63
|
+
export declare function allConnectionFailures(failures: ReadonlyArray<RunFailureClass | null>): failures is ReadonlyArray<RunFailureClass & {
|
|
64
|
+
kind: 'connection';
|
|
65
|
+
}>;
|
|
66
|
+
//# sourceMappingURL=failure-kind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure-kind.d.ts","sourceRoot":"","sources":["../src/failure-kind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAA;AAElE,iEAAiE;AACjE,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAA;AAE7F,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,IAAI,EAAE,cAAc,CAAA;IACpB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,qBAAqB,CAAA;IAClC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AA0ED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAoBvE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,CAAC,eAAe,GAAG,IAAI,CAAC,GAC9C,QAAQ,IAAI,aAAa,CAAC,eAAe,GAAG;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAErE"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failing request actually hit, read off the runner's own output.
|
|
3
|
+
*
|
|
4
|
+
* Three layers fail in three different ways, and only one of them is the app's code:
|
|
5
|
+
*
|
|
6
|
+
* - **connection** — the request never got an answer at all: the transport timed out, was
|
|
7
|
+
* refused, reset, or could not resolve the name or negotiate TLS. Nothing about a handler,
|
|
8
|
+
* a header or CORS was measured, so a re-run is the only thing that tells a cold start (a
|
|
9
|
+
* machine deployed minutes ago whose boot finishes after the URL is live) from a real outage.
|
|
10
|
+
* - **status** — a response arrived and the HTTP status disagreed with what the suite asserts.
|
|
11
|
+
* - **assertion** — a response arrived and an assertion over its body failed.
|
|
12
|
+
*
|
|
13
|
+
* X0 xr-molecule-flash-m110 (+18:42 → +20:09) is the case this exists for: one Hurl file on the
|
|
14
|
+
* freshly deployed URL failed with `(28) Operation timed out … 0 bytes received` while the other
|
|
15
|
+
* twelve passed and 138 requests executed, and every note on the result read it as an assertion.
|
|
16
|
+
* The executor rewrote a CORS middleware, bisected a second server and latency-compared the two,
|
|
17
|
+
* then re-ran the same file — which passed in 3 s. A single re-run was the whole fix, and the one
|
|
18
|
+
* fact that would have said so is the failure's LAYER.
|
|
19
|
+
*
|
|
20
|
+
* ONE classifier, used by the causes note ({@link ./utilities.ts} `failureCausesNote`) and by the
|
|
21
|
+
* platform's own proof result (`molecule-dev` `suite-proof.ts`), so the two can never disagree
|
|
22
|
+
* about whether a failure was the app's to fix.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Transport-level failures, most specific first: the patterns curl/Hurl and Chromium/Node print
|
|
28
|
+
* for a request that never got an answer. A message can carry more than one (`page.goto: Timeout
|
|
29
|
+
* 30000ms exceeded … net::ERR_CONNECTION_REFUSED`), and the more specific reading wins.
|
|
30
|
+
*/
|
|
31
|
+
const CONNECTION_FAILURES = [
|
|
32
|
+
// Hurl/curl's numbered errors (curl's exit codes, printed as `(28) Operation timed out …`).
|
|
33
|
+
['empty', /\(\s*52\)\s*Empty reply from server/],
|
|
34
|
+
['timeout', /\(\s*28\)\s*Operation timed out|Operation timed out after \d+/],
|
|
35
|
+
['dns', /\(\s*6\)\s*Could not resolve host|Could not resolve host/],
|
|
36
|
+
[
|
|
37
|
+
'tls',
|
|
38
|
+
/\(\s*(?:35|51|58|59|60|77|83|90|91)\)\s|SSL connect error|SSL certificate problem|self[- ]signed certificate|UNABLE_TO_VERIFY_LEAF_SIGNATURE|CERT_HAS_EXPIRED/i,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
'refused',
|
|
42
|
+
/\(\s*7\)\s*(?:Failed to connect|Couldn't connect)|Failed to connect to\s|Couldn't connect to server/,
|
|
43
|
+
],
|
|
44
|
+
['reset', /\(\s*5[56]\)\s*(?:Recv|Send) failure|Connection reset by peer|socket hang up/],
|
|
45
|
+
// The browser's own net errors, and the Node errors underneath them.
|
|
46
|
+
['timeout', /net::ERR_(?:CONNECTION_)?TIMED_OUT|\bETIMEDOUT\b/],
|
|
47
|
+
['dns', /net::ERR_NAME_(?:NOT_RESOLVED|RESOLUTION_FAILED)|\bENOTFOUND\b|\bEAI_AGAIN\b/],
|
|
48
|
+
['refused', /net::ERR_CONNECTION_REFUSED|\bECONNREFUSED\b/],
|
|
49
|
+
['empty', /net::ERR_EMPTY_RESPONSE/],
|
|
50
|
+
['reset', /net::ERR_CONNECTION_(?:RESET|CLOSED|ABORTED)|\bECONNRESET\b/],
|
|
51
|
+
['tls', /net::ERR_(?:CERT|SSL|BAD_SSL)\w*/i],
|
|
52
|
+
];
|
|
53
|
+
/** A status assertion: the response arrived and its code is what failed. */
|
|
54
|
+
const STATUS_FAILURE = /^\s*error:\s*Assert status code\b|actual value is <\d{3}>|\bHTTP(?:\/[\d.]+)?\s+[1-5]\d{2}\b|Expected[: ]+[1-5]\d{2}\b|\.status\(\)/m;
|
|
55
|
+
/** Any other assertion over a response that arrived. */
|
|
56
|
+
const ASSERTION_FAILURE = /^\s*error:\s*Assert\b|\bAssertionError\b|\bexpect\(|\bExpected[: ]|\bReceived[: ]|\bjsonpath\b|^\s*Error:/m;
|
|
57
|
+
/** Hurl's `--> file:line:col`, the location of the failing request or assertion. */
|
|
58
|
+
const HURL_AT = /-->\s+(\S+?):(\d+)(?::\d+)?\s*$/m;
|
|
59
|
+
/** A spec file and line, the location a Playwright block names. */
|
|
60
|
+
const SPEC_AT = /([\w./~-]+\.(?:spec|test)\.[cm]?[jt]sx?):(\d+)(?::\d+)?/;
|
|
61
|
+
/** How long a request waited, as curl prints it (`Operation timed out after 20002 milliseconds`). */
|
|
62
|
+
const WAITED_MS = /Operation timed out after (\d+) milliseconds/;
|
|
63
|
+
/** How long a request waited, as Playwright prints it (`Timeout 30000ms exceeded`). */
|
|
64
|
+
const WAITED_PLAYWRIGHT = /(?<!Test )\bTimeout (\d+)ms exceeded/;
|
|
65
|
+
/**
|
|
66
|
+
* The `file:line` a failure block names, when it names one.
|
|
67
|
+
*
|
|
68
|
+
* @param text - The failure block.
|
|
69
|
+
* @returns The location, or undefined when the block carries none.
|
|
70
|
+
*/
|
|
71
|
+
function failureLocation(text) {
|
|
72
|
+
const hurl = HURL_AT.exec(text);
|
|
73
|
+
if (hurl)
|
|
74
|
+
return `${hurl[1]}:${hurl[2]}`;
|
|
75
|
+
const spec = SPEC_AT.exec(text);
|
|
76
|
+
return spec ? `${spec[1]}:${spec[2]}` : undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* How long the request waited before it gave up, in seconds, when the runner printed it.
|
|
80
|
+
*
|
|
81
|
+
* @param text - The failure block.
|
|
82
|
+
* @returns The seconds, rounded, or undefined.
|
|
83
|
+
*/
|
|
84
|
+
function waitedSeconds(text) {
|
|
85
|
+
const ms = WAITED_MS.exec(text) ?? WAITED_PLAYWRIGHT.exec(text);
|
|
86
|
+
return ms ? Math.round(Number(ms[1]) / 1000) : undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Classify ONE failure's text — a Hurl `error:` block, or a Playwright failure message/body.
|
|
90
|
+
*
|
|
91
|
+
* The transport is read first: a block that carries a connection error AND an assertion is a
|
|
92
|
+
* request whose assertion never ran, so it is a connection failure. Only a block that carries no
|
|
93
|
+
* failure signature at all answers null — unknown is never a connection failure, because a
|
|
94
|
+
* misread transport hides a real app failure behind "just re-run it".
|
|
95
|
+
*
|
|
96
|
+
* @param text - One failure's text, as the runner printed it.
|
|
97
|
+
* @returns The classification, or null when the text carries no failure this reads.
|
|
98
|
+
*/
|
|
99
|
+
export function classifyRunFailure(text) {
|
|
100
|
+
const body = String(text ?? '');
|
|
101
|
+
if (body.trim().length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
for (const [connection, pattern] of CONNECTION_FAILURES) {
|
|
104
|
+
if (!pattern.test(body))
|
|
105
|
+
continue;
|
|
106
|
+
const result = { kind: 'connection', connection };
|
|
107
|
+
const at = failureLocation(body);
|
|
108
|
+
if (at !== undefined)
|
|
109
|
+
result.at = at;
|
|
110
|
+
const seconds = waitedSeconds(body);
|
|
111
|
+
if (seconds !== undefined)
|
|
112
|
+
result.seconds = seconds;
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
const at = failureLocation(body);
|
|
116
|
+
const kind = STATUS_FAILURE.test(body)
|
|
117
|
+
? 'status'
|
|
118
|
+
: ASSERTION_FAILURE.test(body)
|
|
119
|
+
? 'assertion'
|
|
120
|
+
: null;
|
|
121
|
+
if (kind === null)
|
|
122
|
+
return null;
|
|
123
|
+
return at === undefined ? { kind } : { kind, at };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Whether every one of the classified failures is a connection failure — the only shape in which
|
|
127
|
+
* no response the app produced was measured.
|
|
128
|
+
*
|
|
129
|
+
* @param failures - The run's classifications, in printed order.
|
|
130
|
+
* @returns True when there is at least one and none of them is the app's.
|
|
131
|
+
*/
|
|
132
|
+
export function allConnectionFailures(failures) {
|
|
133
|
+
return failures.length > 0 && failures.every((failure) => failure?.kind === 'connection');
|
|
134
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -134,9 +134,11 @@ export * from './backends/sandbox.js';
|
|
|
134
134
|
export * from './browser-guard.js';
|
|
135
135
|
export * from './command-position.js';
|
|
136
136
|
export * from './cut-run.js';
|
|
137
|
+
export * from './failure-kind.js';
|
|
137
138
|
export * from './input-normalizer.js';
|
|
138
139
|
export * from './no-test-run.js';
|
|
139
140
|
export * from './schemas.js';
|
|
141
|
+
export * from './sleep-peek.js';
|
|
140
142
|
export * from './suite-overlap.js';
|
|
141
143
|
export * from './system-prompt.js';
|
|
142
144
|
export * from './tools.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,sBAAsB,CAAA;AACpC,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,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,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
|
@@ -134,9 +134,11 @@ export * from './backends/sandbox.js';
|
|
|
134
134
|
export * from './browser-guard.js';
|
|
135
135
|
export * from './command-position.js';
|
|
136
136
|
export * from './cut-run.js';
|
|
137
|
+
export * from './failure-kind.js';
|
|
137
138
|
export * from './input-normalizer.js';
|
|
138
139
|
export * from './no-test-run.js';
|
|
139
140
|
export * from './schemas.js';
|
|
141
|
+
export * from './sleep-peek.js';
|
|
140
142
|
export * from './suite-overlap.js';
|
|
141
143
|
export * from './system-prompt.js';
|
|
142
144
|
export * from './tools.js';
|