@molecule/api-ai-tools 1.5.14 → 1.5.16
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 +95 -3
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +75 -16
- package/dist/utilities.d.ts +59 -2
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +176 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-16T07:29:43.225Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -572,6 +572,22 @@ function redactSecretsInCode(s: string): string
|
|
|
572
572
|
|
|
573
573
|
**Returns:** A redacted copy that preserves ordinary code verbatim.
|
|
574
574
|
|
|
575
|
+
#### `printsOnlySource(command)`
|
|
576
|
+
|
|
577
|
+
Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
|
|
578
|
+
`head -50 b.hurl`): its output is file content and gets the code-safe redaction, like
|
|
579
|
+
read_file. Through the env-dump grade a printed `type AuthState = 'authenticated' | …`
|
|
580
|
+
came back as `'[REDACTED]'`, and the executor wrote the mask into a new file (X1
|
|
581
|
+
flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
function printsOnlySource(command: string): boolean
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
- `command` — The shell command.
|
|
588
|
+
|
|
589
|
+
**Returns:** True when every segment prints source files only.
|
|
590
|
+
|
|
575
591
|
#### `isEnvFilePath(path)`
|
|
576
592
|
|
|
577
593
|
Whether a path is an env file, for which {@link redactSecrets}' full env-dump
|
|
@@ -646,9 +662,16 @@ killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
|
|
|
646
662
|
- a trailing `| tail -N`, which holds everything back until the command ends.
|
|
647
663
|
It is dropped: `wait_for_task` already returns the end of the log.
|
|
648
664
|
- `playwright test` with no `--reporter`, which runs the config's reporter —
|
|
649
|
-
often `html`, which prints nothing per test. `--reporter=
|
|
665
|
+
often `html`, which prints nothing per test. `--reporter=list` is added
|
|
650
666
|
when the playwright run is the command's last step and nothing is piped or
|
|
651
|
-
redirected from it
|
|
667
|
+
redirected from it: it writes each result as it finishes, and it is the
|
|
668
|
+
shape the platform reads a background suite back in (`suite-fail-fast.ts`'s
|
|
669
|
+
fail-fast note and the Tests bar both parse `list` lines). A run that names
|
|
670
|
+
`--reporter=line` is switched to `list` for the same reason — `line` writes
|
|
671
|
+
`[n/N] <title>` with no ✓/✘ mark and no duration, so on X1 flash-m62 an
|
|
672
|
+
8.3-minute walk of one suite could never trip the fail-fast note.
|
|
673
|
+
|
|
674
|
+
Only the command's last step is touched, and only its reporter.
|
|
652
675
|
|
|
653
676
|
```typescript
|
|
654
677
|
function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
|
|
@@ -827,6 +850,25 @@ function failureBlocks(s: string): string[]
|
|
|
827
850
|
|
|
828
851
|
**Returns:** Each block, in order (empty when there are none).
|
|
829
852
|
|
|
853
|
+
#### `runningOutputSliceCommand(log, own)`
|
|
854
|
+
|
|
855
|
+
The shell command whose stdout is the interesting part of a log that is still being written:
|
|
856
|
+
the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
|
|
857
|
+
|
|
858
|
+
A Playwright `list` run prints one line per test as it finishes and its error blocks only at
|
|
859
|
+
the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
|
|
860
|
+
the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
|
|
861
|
+
them, which cost the run the grep it had to make anyway.
|
|
862
|
+
|
|
863
|
+
```typescript
|
|
864
|
+
function runningOutputSliceCommand(log: string, own?: string): string
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
- `log` — The task log path.
|
|
868
|
+
- `own` — A file the command redirects its own output to, if any.
|
|
869
|
+
|
|
870
|
+
**Returns:** The shell command, safe to hand to the execution backend as-is.
|
|
871
|
+
|
|
830
872
|
#### `whitespaceTolerantReplace(content, oldString, newString)`
|
|
831
873
|
|
|
832
874
|
Attempt a whitespace-tolerant replacement when an exact `old_string` match
|
|
@@ -887,6 +929,22 @@ function parseCheckProblem(result: {
|
|
|
887
929
|
|
|
888
930
|
**Returns:** The message for the tool result, or null.
|
|
889
931
|
|
|
932
|
+
#### `fusedLine(content, newString)`
|
|
933
|
+
|
|
934
|
+
The line a replacement fused when it dropped the newline its `old_string`
|
|
935
|
+
ended with: `new_string`'s text is followed on the same line by what used to
|
|
936
|
+
be the next line. Read off the file as it now is, so the line shown is the
|
|
937
|
+
line the parser is actually complaining about.
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
function fusedLine(content: string, newString: string): { line: number; text: string } | null
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
- `content` — The file's content after the edit.
|
|
944
|
+
- `newString` — A replacement's `new_string`, whose `old_string` ended with a newline.
|
|
945
|
+
|
|
946
|
+
**Returns:** The fused line's 1-based number and its text, or null when the replacement's text is not followed by more of the same line (it ends the file, the following newline survived, or another replacement rewrote it).
|
|
947
|
+
|
|
890
948
|
### Constants
|
|
891
949
|
|
|
892
950
|
#### `TOOL_SCHEMAS`
|
|
@@ -1015,6 +1073,31 @@ const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
|
|
|
1015
1073
|
]
|
|
1016
1074
|
```
|
|
1017
1075
|
|
|
1076
|
+
#### `RUNNING_FAILURE_LINES`
|
|
1077
|
+
|
|
1078
|
+
Failure lines shown from a log that is still being written.
|
|
1079
|
+
|
|
1080
|
+
```typescript
|
|
1081
|
+
const RUNNING_FAILURE_LINES: 40
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
#### `RUNNING_TAIL_BYTES`
|
|
1085
|
+
|
|
1086
|
+
Bytes of a running log's end appended after its failure lines.
|
|
1087
|
+
|
|
1088
|
+
```typescript
|
|
1089
|
+
const RUNNING_TAIL_BYTES: 1000
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
#### `RUNNING_FAILURE_LINE_RE`
|
|
1093
|
+
|
|
1094
|
+
The lines worth showing from a log that is still being written: a failed test's mark or
|
|
1095
|
+
numbered block header, and a runner's own error lines. Matched with `grep -E`.
|
|
1096
|
+
|
|
1097
|
+
```typescript
|
|
1098
|
+
const RUNNING_FAILURE_LINE_RE: '^[[:space:]]*(✘|✗|×|✖|✕|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)'
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1018
1101
|
#### `PARSE_CHECKABLE`
|
|
1019
1102
|
|
|
1020
1103
|
Files a parse check can vouch for cheaply right after a write.
|
|
@@ -1061,6 +1144,15 @@ Peer dependencies:
|
|
|
1061
1144
|
Line numbers count the way `read_file` does, so they are valid offsets. The
|
|
1062
1145
|
excerpt is redacted like a read. The existing fields (`ok`,
|
|
1063
1146
|
`replacementsApplied`, `alreadyApplied`, `syntaxError`) are unchanged.
|
|
1147
|
+
- **A dropped line break and the parse error it causes are reported as ONE
|
|
1148
|
+
thing.** When a replacement's `old_string` ended with a newline and its
|
|
1149
|
+
`new_string` does not (so the next line is pulled up onto it) and the file
|
|
1150
|
+
then fails its parse check, `edit_file` returns a single `syntaxError` that
|
|
1151
|
+
names the replacement, shows the fused line with its number, and says to
|
|
1152
|
+
re-send that replacement with the newline restored — instead of the
|
|
1153
|
+
`newlineNote` hint beside an unrelated-looking parse error, which an executor
|
|
1154
|
+
repairs by rewriting the fused line's text and fuses the next one. The
|
|
1155
|
+
`newlineNote` alone is still returned when the file parses.
|
|
1064
1156
|
- **A parameter under another name is not a broken tool.** Every tool's input
|
|
1065
1157
|
is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
1066
1158
|
snake_case), and a handler that throws returns an error naming the missing
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,15 @@
|
|
|
38
38
|
* Line numbers count the way `read_file` does, so they are valid offsets. The
|
|
39
39
|
* excerpt is redacted like a read. The existing fields (`ok`,
|
|
40
40
|
* `replacementsApplied`, `alreadyApplied`, `syntaxError`) are unchanged.
|
|
41
|
+
* - **A dropped line break and the parse error it causes are reported as ONE
|
|
42
|
+
* thing.** When a replacement's `old_string` ended with a newline and its
|
|
43
|
+
* `new_string` does not (so the next line is pulled up onto it) and the file
|
|
44
|
+
* then fails its parse check, `edit_file` returns a single `syntaxError` that
|
|
45
|
+
* names the replacement, shows the fused line with its number, and says to
|
|
46
|
+
* re-send that replacement with the newline restored — instead of the
|
|
47
|
+
* `newlineNote` hint beside an unrelated-looking parse error, which an executor
|
|
48
|
+
* repairs by rewriting the fused line's text and fuses the next one. The
|
|
49
|
+
* `newlineNote` alone is still returned when the file parses.
|
|
41
50
|
* - **A parameter under another name is not a broken tool.** Every tool's input
|
|
42
51
|
* is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
43
52
|
* snake_case), and a handler that throws returns an error naming the missing
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,15 @@
|
|
|
38
38
|
* Line numbers count the way `read_file` does, so they are valid offsets. The
|
|
39
39
|
* excerpt is redacted like a read. The existing fields (`ok`,
|
|
40
40
|
* `replacementsApplied`, `alreadyApplied`, `syntaxError`) are unchanged.
|
|
41
|
+
* - **A dropped line break and the parse error it causes are reported as ONE
|
|
42
|
+
* thing.** When a replacement's `old_string` ended with a newline and its
|
|
43
|
+
* `new_string` does not (so the next line is pulled up onto it) and the file
|
|
44
|
+
* then fails its parse check, `edit_file` returns a single `syntaxError` that
|
|
45
|
+
* names the replacement, shows the fused line with its number, and says to
|
|
46
|
+
* re-send that replacement with the newline restored — instead of the
|
|
47
|
+
* `newlineNote` hint beside an unrelated-looking parse error, which an executor
|
|
48
|
+
* repairs by rewriting the fused line's text and fuses the next one. The
|
|
49
|
+
* `newlineNote` alone is still returned when the file parses.
|
|
41
50
|
* - **A parameter under another name is not a broken tool.** Every tool's input
|
|
42
51
|
* is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
43
52
|
* snake_case), and a handler that throws returns an error naming the missing
|
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;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,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;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA6CnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAupDxF"}
|
package/dist/tools.js
CHANGED
|
@@ -25,7 +25,7 @@ const waitedByTask = new Map();
|
|
|
25
25
|
const logBytesByTask = new Map();
|
|
26
26
|
/** The file a background command writes its own output to, when it redirects it. */
|
|
27
27
|
const outputFileByTask = new Map();
|
|
28
|
-
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
|
|
28
|
+
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, fusedLine, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, redactSecrets, redactSecretsInCode, resolvePath, RUNNING_FAILURE_LINES, runningOutputSliceCommand, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
|
|
29
29
|
/**
|
|
30
30
|
* How long exec_command waits for a command it backgrounded only because the
|
|
31
31
|
* command asked for more than the tool's ceiling, before handing back a task id.
|
|
@@ -876,24 +876,69 @@ export function buildTools(backend, config) {
|
|
|
876
876
|
// A replacement whose old text ended in a newline and whose new text does not
|
|
877
877
|
// joins the line after it onto its last line. Applied as written, but said:
|
|
878
878
|
// flash-m28 saw two lines merged only one round trip later.
|
|
879
|
-
const
|
|
879
|
+
const dropped = replacements
|
|
880
|
+
.map((r, i) => ({ ...r, position: i + 1 }))
|
|
881
|
+
.filter((r) => typeof r.old_string === 'string' &&
|
|
880
882
|
typeof r.new_string === 'string' &&
|
|
881
883
|
r.old_string.endsWith('\n') &&
|
|
882
884
|
r.new_string !== '' &&
|
|
883
|
-
!r.new_string.endsWith('\n'))
|
|
885
|
+
!r.new_string.endsWith('\n'));
|
|
886
|
+
const syntax = await syntaxErrorAfterWrite(path);
|
|
887
|
+
// Those two facts are ONE accident, and reported apart the model repairs the
|
|
888
|
+
// wrong one: flash-m55 dropped the boundary on two consecutive edit_file calls,
|
|
889
|
+
// both left `handlers/conduit.ts` unparseable, and it fixed the fused LINE's
|
|
890
|
+
// text — which re-fused the next line in the same batch. When the parse error
|
|
891
|
+
// follows a dropped newline, name the fused line and the single edit that
|
|
892
|
+
// undoes it, instead of a style note beside an unrelated-looking parse error.
|
|
893
|
+
const which = dropped.length === 1
|
|
894
|
+
? `replacement ${dropped[0].position}`
|
|
895
|
+
: `replacements ${dropped.map((r) => r.position).join(', ')}`;
|
|
896
|
+
const located = dropped.flatMap((r) => {
|
|
897
|
+
const line = fusedLine(finalContent, r.new_string);
|
|
898
|
+
return line ? [line] : [];
|
|
899
|
+
});
|
|
900
|
+
const fused = located.slice(0, 3);
|
|
901
|
+
const fusedOmitted = located.length - fused.length;
|
|
902
|
+
const shownFused = fused
|
|
903
|
+
.map((l) => ` ${l.line}: ${l.text.length > 200 ? `${l.text.slice(0, 200)}…` : l.text}`)
|
|
904
|
+
.join('\n');
|
|
905
|
+
const fusedNote = syntax.syntaxError && dropped.length > 0
|
|
906
|
+
? 'The file no longer parses after this change, and ' +
|
|
907
|
+
`${which} removed the line break that followed the replaced text — so the next line ` +
|
|
908
|
+
'now continues on the line before it, and that fused line is what the parser rejects:\n' +
|
|
909
|
+
(shownFused || ` (not located as the file now reads — read_file ${path})`) +
|
|
910
|
+
(fusedOmitted > 0
|
|
911
|
+
? `\n (${fusedOmitted} more fused line(s) — read_file ${path})`
|
|
912
|
+
: '') +
|
|
913
|
+
`\nRe-send ${which} with the trailing newline restored on new_string (change nothing ` +
|
|
914
|
+
"else about it). Editing the FUSED LINE's text instead fuses the next line the same " +
|
|
915
|
+
`way (flash-m55: three edits in a row).\n${syntax.syntaxError}`
|
|
916
|
+
: null;
|
|
884
917
|
return {
|
|
885
918
|
path,
|
|
886
919
|
ok: true,
|
|
887
920
|
replacementsApplied: replacements.length - alreadyApplied,
|
|
888
|
-
...(
|
|
889
|
-
? {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
921
|
+
...(fusedNote
|
|
922
|
+
? { syntaxError: fusedNote }
|
|
923
|
+
: {
|
|
924
|
+
// The evidence for this note is `located`, not `dropped`: a drop joins
|
|
925
|
+
// two lines only when something other than a newline (or EOF) follows
|
|
926
|
+
// the replaced text. A weaker model writes one line's change as two
|
|
927
|
+
// replacements — the line including its trailing newline, then the
|
|
928
|
+
// line's own text — and the newline it consumed ended that line with a
|
|
929
|
+
// BLANK line after it, so nothing joined while the model was told it
|
|
930
|
+
// had: it spent a new round trip putting the blank line back
|
|
931
|
+
// (flash-m62). Say nothing when nothing was joined.
|
|
932
|
+
...(dropped.length > 0 && located.length > 0
|
|
933
|
+
? {
|
|
934
|
+
newlineNote: `${dropped.length === 1 ? 'A replacement' : `${dropped.length} replacements`} removed the line break after the replaced text (old_string ended with a newline, new_string does not), so the next line now continues on the same line. Check the excerpt; add the newline back if that was not intended.`,
|
|
935
|
+
}
|
|
936
|
+
: {}),
|
|
937
|
+
...syntax,
|
|
938
|
+
}),
|
|
893
939
|
...(alreadyApplied > 0 ? { alreadyApplied, note: alreadyNote } : {}),
|
|
894
940
|
totalLines: countLines(finalContent),
|
|
895
941
|
...(excerpt ? { excerpt: sanitizeFileContent(excerpt, path) } : {}),
|
|
896
|
-
...(await syntaxErrorAfterWrite(path)),
|
|
897
942
|
};
|
|
898
943
|
}
|
|
899
944
|
catch (e) {
|
|
@@ -929,7 +974,9 @@ export function buildTools(backend, config) {
|
|
|
929
974
|
// search found nothing (X1 run sol-m07). A pattern that is not a valid
|
|
930
975
|
// extended regex (`foo(` — an unbalanced paren) makes grep exit 2; it is
|
|
931
976
|
// then searched as the plain text it was meant as.
|
|
932
|
-
|
|
977
|
+
// -H: a search scoped to one file printed `line:content` with no file, and every match
|
|
978
|
+
// came back `{file: "", line: 0}` (X1 flash-m62).
|
|
979
|
+
const grepArgs = `-rnH ${globArg} ${contextArg} ${grepExcludeArgs(path)} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)}`;
|
|
933
980
|
const result = await backend.run(`{ grep -E ${grepArgs}; s=$?; if [ "$s" -eq 2 ]; then grep -F ${grepArgs}; fi; } 2>/dev/null || true`, { timeout: 10000 });
|
|
934
981
|
// grep emits `<file>:<line>:<content>`, so redaction runs PER MATCH on the
|
|
935
982
|
// content alone: the file prefix would otherwise hide an env assignment from
|
|
@@ -1231,7 +1278,12 @@ export function buildTools(backend, config) {
|
|
|
1231
1278
|
// truncateMiddle (not truncate): a failing build/test/migration puts its
|
|
1232
1279
|
// error at the TAIL, so keep the head AND the tail — head-only truncation
|
|
1233
1280
|
// strands the executor with passing progress and no failure reason.
|
|
1234
|
-
|
|
1281
|
+
// A command that only prints source files returns file content: the code-safe grade,
|
|
1282
|
+
// as read_file uses (utilities.ts printsOnlySource).
|
|
1283
|
+
const grade = printsOnlySource(command)
|
|
1284
|
+
? (s) => sanitizeFileContent(s, 'printed.ts')
|
|
1285
|
+
: sanitizeOutput;
|
|
1286
|
+
const stdout = grade(truncateMiddle(result.stdout, MAX_OUTPUT_SIZE));
|
|
1235
1287
|
const stderr = sanitizeOutput(truncateMiddle(result.stderr, MAX_OUTPUT_SIZE));
|
|
1236
1288
|
if (budgetSeconds && result.exitCode === 124) {
|
|
1237
1289
|
return {
|
|
@@ -1419,15 +1471,17 @@ export function buildTools(backend, config) {
|
|
|
1419
1471
|
};
|
|
1420
1472
|
}
|
|
1421
1473
|
// The output so far rides the result: a wait that shows nothing was waited blind
|
|
1422
|
-
// (X1 flash-m50: 4m42s across two waits on an 11-minute run).
|
|
1474
|
+
// (X1 flash-m50: 4m42s across two waits on an 11-minute run). The slice leads with the
|
|
1475
|
+
// failure lines, not the last bytes — a suite's tail is its progress lines
|
|
1476
|
+
// (X1 flash-m61: the last 3 000 bytes of a 139-test walk were the skip list).
|
|
1423
1477
|
const own = outputFileByTask.get(taskId);
|
|
1424
1478
|
let soFar;
|
|
1425
1479
|
try {
|
|
1426
|
-
const r = await backend.run(
|
|
1480
|
+
const r = await backend.run(runningOutputSliceCommand(log, own), { timeout: 5_000 });
|
|
1427
1481
|
soFar = r.stdout ?? '';
|
|
1428
1482
|
}
|
|
1429
1483
|
catch (_error) {
|
|
1430
|
-
// No
|
|
1484
|
+
// No slice: the note still says how to peek.
|
|
1431
1485
|
soFar = '';
|
|
1432
1486
|
}
|
|
1433
1487
|
return {
|
|
@@ -1436,8 +1490,13 @@ export function buildTools(backend, config) {
|
|
|
1436
1490
|
waitedMs,
|
|
1437
1491
|
...(soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {}),
|
|
1438
1492
|
note: `Still running after ${Math.round(waitedMs / 1000)}s. Call wait_for_task again to ` +
|
|
1439
|
-
`keep waiting — its result carries the log output
|
|
1440
|
-
|
|
1493
|
+
`keep waiting — its result carries the log output. ` +
|
|
1494
|
+
(soFar.trim()
|
|
1495
|
+
? `outputSoFar is the failure lines so far (up to ${RUNNING_FAILURE_LINES}) and then the end of ` +
|
|
1496
|
+
`the log, not the whole run — \`grep -n '✘' ${log} | head -40\` lists every failure, ` +
|
|
1497
|
+
`\`grep -n -A 20 '<test name>' ${log}\` reads one failure's error. `
|
|
1498
|
+
: `Nothing has been printed yet — \`grep -n '✘' ${log} | head -40\` lists failures as they appear. `) +
|
|
1499
|
+
`Meanwhile do work that leaves the code under test alone (read the failures so far, plan the fixes): editing app/src or api/src now changes the code the run is testing, and its result no longer counts.`,
|
|
1441
1500
|
};
|
|
1442
1501
|
}
|
|
1443
1502
|
let output;
|
package/dist/utilities.d.ts
CHANGED
|
@@ -55,6 +55,17 @@ export declare function redactSecrets(s: string): string;
|
|
|
55
55
|
* @returns A redacted copy that preserves ordinary code verbatim.
|
|
56
56
|
*/
|
|
57
57
|
export declare function redactSecretsInCode(s: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
|
|
60
|
+
* `head -50 b.hurl`): its output is file content and gets the code-safe redaction, like
|
|
61
|
+
* read_file. Through the env-dump grade a printed `type AuthState = 'authenticated' | …`
|
|
62
|
+
* came back as `'[REDACTED]'`, and the executor wrote the mask into a new file (X1
|
|
63
|
+
* flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
|
|
64
|
+
*
|
|
65
|
+
* @param command - The shell command.
|
|
66
|
+
* @returns True when every segment prints source files only.
|
|
67
|
+
*/
|
|
68
|
+
export declare function printsOnlySource(command: string): boolean;
|
|
58
69
|
/**
|
|
59
70
|
* Whether a path is an env file, for which {@link redactSecrets}' full env-dump
|
|
60
71
|
* treatment is appropriate rather than {@link redactSecretsInCode}. Matches
|
|
@@ -108,9 +119,16 @@ export declare function outputRedirectTarget(command: string): string | null;
|
|
|
108
119
|
* - a trailing `| tail -N`, which holds everything back until the command ends.
|
|
109
120
|
* It is dropped: `wait_for_task` already returns the end of the log.
|
|
110
121
|
* - `playwright test` with no `--reporter`, which runs the config's reporter —
|
|
111
|
-
* often `html`, which prints nothing per test. `--reporter=
|
|
122
|
+
* often `html`, which prints nothing per test. `--reporter=list` is added
|
|
112
123
|
* when the playwright run is the command's last step and nothing is piped or
|
|
113
|
-
* redirected from it
|
|
124
|
+
* redirected from it: it writes each result as it finishes, and it is the
|
|
125
|
+
* shape the platform reads a background suite back in (`suite-fail-fast.ts`'s
|
|
126
|
+
* fail-fast note and the Tests bar both parse `list` lines). A run that names
|
|
127
|
+
* `--reporter=line` is switched to `list` for the same reason — `line` writes
|
|
128
|
+
* `[n/N] <title>` with no ✓/✘ mark and no duration, so on X1 flash-m62 an
|
|
129
|
+
* 8.3-minute walk of one suite could never trip the fail-fast note.
|
|
130
|
+
*
|
|
131
|
+
* Only the command's last step is touched, and only its reporter.
|
|
114
132
|
*
|
|
115
133
|
* @param command - The command as the executor sent it.
|
|
116
134
|
* @returns The command to run and one sentence per change made (empty when none).
|
|
@@ -296,6 +314,29 @@ export declare function truncateMiddle(s: string, maxLength: number): string;
|
|
|
296
314
|
* @returns Each block, in order (empty when there are none).
|
|
297
315
|
*/
|
|
298
316
|
export declare function failureBlocks(s: string): string[];
|
|
317
|
+
/** Failure lines shown from a log that is still being written. */
|
|
318
|
+
export declare const RUNNING_FAILURE_LINES = 40;
|
|
319
|
+
/** Bytes of a running log's end appended after its failure lines. */
|
|
320
|
+
export declare const RUNNING_TAIL_BYTES = 1000;
|
|
321
|
+
/**
|
|
322
|
+
* The lines worth showing from a log that is still being written: a failed test's mark or
|
|
323
|
+
* numbered block header, and a runner's own error lines. Matched with `grep -E`.
|
|
324
|
+
*/
|
|
325
|
+
export declare const RUNNING_FAILURE_LINE_RE = "^[[:space:]]*(\u2718|\u2717|\u00D7|\u2716|\u2715|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)";
|
|
326
|
+
/**
|
|
327
|
+
* The shell command whose stdout is the interesting part of a log that is still being written:
|
|
328
|
+
* the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
|
|
329
|
+
*
|
|
330
|
+
* A Playwright `list` run prints one line per test as it finishes and its error blocks only at
|
|
331
|
+
* the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
|
|
332
|
+
* the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
|
|
333
|
+
* them, which cost the run the grep it had to make anyway.
|
|
334
|
+
*
|
|
335
|
+
* @param log - The task log path.
|
|
336
|
+
* @param own - A file the command redirects its own output to, if any.
|
|
337
|
+
* @returns The shell command, safe to hand to the execution backend as-is.
|
|
338
|
+
*/
|
|
339
|
+
export declare function runningOutputSliceCommand(log: string, own?: string): string;
|
|
299
340
|
/**
|
|
300
341
|
* Attempt a whitespace-tolerant replacement when an exact `old_string` match
|
|
301
342
|
* failed. Finds a contiguous run of lines in `content` whose per-line
|
|
@@ -339,4 +380,20 @@ export declare function parseCheckProblem(result: {
|
|
|
339
380
|
stderr: string;
|
|
340
381
|
exitCode: number;
|
|
341
382
|
}): string | null;
|
|
383
|
+
/**
|
|
384
|
+
* The line a replacement fused when it dropped the newline its `old_string`
|
|
385
|
+
* ended with: `new_string`'s text is followed on the same line by what used to
|
|
386
|
+
* be the next line. Read off the file as it now is, so the line shown is the
|
|
387
|
+
* line the parser is actually complaining about.
|
|
388
|
+
*
|
|
389
|
+
* @param content - The file's content after the edit.
|
|
390
|
+
* @param newString - A replacement's `new_string`, whose `old_string` ended with a newline.
|
|
391
|
+
* @returns The fused line's 1-based number and its text, or null when the
|
|
392
|
+
* replacement's text is not followed by more of the same line (it ends the
|
|
393
|
+
* file, the following newline survived, or another replacement rewrote it).
|
|
394
|
+
*/
|
|
395
|
+
export declare function fusedLine(content: string, newString: string): {
|
|
396
|
+
line: number;
|
|
397
|
+
text: string;
|
|
398
|
+
} | null;
|
|
342
399
|
//# sourceMappingURL=utilities.d.ts.map
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAEE,CAAA;
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
|
package/dist/utilities.js
CHANGED
|
@@ -98,11 +98,21 @@ const NON_SECRET_VALUES = new Set([
|
|
|
98
98
|
* @param quote - The quote character the value uses (`"` or `'`).
|
|
99
99
|
* @returns A String.replace replacer preserving allowlisted values.
|
|
100
100
|
*/
|
|
101
|
-
const jsonValueReplacer = (quote) => (match, prefix) => {
|
|
101
|
+
const jsonValueReplacer = (quote) => (match, prefix, offset, whole) => {
|
|
102
102
|
const value = match.slice(prefix.length);
|
|
103
103
|
const inner = value.slice(1, -1);
|
|
104
|
+
// A TypeScript type literal is never a credential: `type AuthState = 'authenticated' | …`
|
|
105
|
+
// (X1 flash-m58). A union member is followed by `|`; a declaration starts `type`.
|
|
106
|
+
if (/^\s*\|/.test(whole.slice(offset + match.length)))
|
|
107
|
+
return match;
|
|
108
|
+
if (/\btype\s+$/.test(whole.slice(Math.max(0, offset - 16), offset)))
|
|
109
|
+
return match;
|
|
104
110
|
// An empty value hides nothing, and a suite fixture's `"password": ""` is the fact a
|
|
105
111
|
// failing assertion turns on (X1 flash-m52).
|
|
112
|
+
// A version (`"jsonwebtoken": "9.0.2"`, a package name that holds a keyword) is not a
|
|
113
|
+
// credential, and a masked one hid which version is installed (X1 flash-m57).
|
|
114
|
+
if (/^[\^~]?\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(inner))
|
|
115
|
+
return match;
|
|
106
116
|
if (inner === '' || NON_SECRET_VALUES.has(inner.toLowerCase()))
|
|
107
117
|
return match;
|
|
108
118
|
return `${prefix}${quote}[REDACTED]${quote}`;
|
|
@@ -149,6 +159,61 @@ export function redactSecrets(s) {
|
|
|
149
159
|
export function redactSecretsInCode(s) {
|
|
150
160
|
return s.replace(SECRET_ENV_ASSIGNMENT, '$1=[REDACTED]');
|
|
151
161
|
}
|
|
162
|
+
/** Source-file extensions a printing command's output is read as code for. */
|
|
163
|
+
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|css|scss|html|vue|svelte|md|sql|hurl)$/;
|
|
164
|
+
/**
|
|
165
|
+
* Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
|
|
166
|
+
* `head -50 b.hurl`): its output is file content and gets the code-safe redaction, like
|
|
167
|
+
* read_file. Through the env-dump grade a printed `type AuthState = 'authenticated' | …`
|
|
168
|
+
* came back as `'[REDACTED]'`, and the executor wrote the mask into a new file (X1
|
|
169
|
+
* flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
|
|
170
|
+
*
|
|
171
|
+
* @param command - The shell command.
|
|
172
|
+
* @returns True when every segment prints source files only.
|
|
173
|
+
*/
|
|
174
|
+
export function printsOnlySource(command) {
|
|
175
|
+
if (/\.env\b/.test(command))
|
|
176
|
+
return false;
|
|
177
|
+
const segments = command
|
|
178
|
+
.split(/\s*(?:&&|;|\n)\s*/)
|
|
179
|
+
.map((s) => s.trim())
|
|
180
|
+
.filter((s) => s && !/^cd\s/.test(s));
|
|
181
|
+
if (segments.length === 0)
|
|
182
|
+
return false;
|
|
183
|
+
return segments.every((seg) => {
|
|
184
|
+
// Words, quotes kept whole: a grep pattern like "sign\\|verify" is one word, not a pipe.
|
|
185
|
+
const words = seg.match(/"(?:\\.|[^"\\])*"|'[^']*'|\S+/g) ?? [];
|
|
186
|
+
const stages = [[]];
|
|
187
|
+
for (const w of words) {
|
|
188
|
+
if (w === '|')
|
|
189
|
+
stages.push([]);
|
|
190
|
+
else if (!/^\d?>&\d$|^2>\/dev\/null$/.test(w))
|
|
191
|
+
stages[stages.length - 1].push(w);
|
|
192
|
+
}
|
|
193
|
+
const [first, ...rest] = stages;
|
|
194
|
+
const head = first[0];
|
|
195
|
+
// A print of named source files: cat/head/tail/nl/sed -n, or a non-recursive grep/rg
|
|
196
|
+
// (X1 flash-m61 read a fixture's password masked through `grep -n … auth.hurl`).
|
|
197
|
+
let args = first.slice(1);
|
|
198
|
+
if (head === 'sed' && args[0] !== '-n')
|
|
199
|
+
return false;
|
|
200
|
+
if (head === 'grep' || head === 'rg') {
|
|
201
|
+
if (args.some((a) => /^-[a-zA-Z]*[rR]/.test(a) || a === '--recursive'))
|
|
202
|
+
return false;
|
|
203
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
204
|
+
const rest2 = args.filter((a) => !a.startsWith('-'));
|
|
205
|
+
args = flags.includes('-e') ? rest2 : rest2.slice(1);
|
|
206
|
+
}
|
|
207
|
+
else if (!/^(?:cat|head|tail|nl|sed)$/.test(head ?? ''))
|
|
208
|
+
return false;
|
|
209
|
+
const files = args.filter((t) => !t.startsWith('-') && !/^'[^']*'$|^"[^"]*"$|^\d+$|^\d+,\d+p$/.test(t));
|
|
210
|
+
if (files.length === 0 || !files.every((f) => SOURCE_FILE_RE.test(f)))
|
|
211
|
+
return false;
|
|
212
|
+
// After it, only text filters that read no file of their own.
|
|
213
|
+
return rest.every((stage) => /^(?:head|tail|sort|uniq|wc|cut|nl|grep)$/.test(stage[0] ?? '') &&
|
|
214
|
+
!stage.slice(1).some((a) => !a.startsWith('-') && /\.[a-z]+$/i.test(a) && !/^['"]/.test(a)));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
152
217
|
/**
|
|
153
218
|
* Whether a path is an env file, for which {@link redactSecrets}' full env-dump
|
|
154
219
|
* treatment is appropriate rather than {@link redactSecretsInCode}. Matches
|
|
@@ -222,6 +287,8 @@ export function outputRedirectTarget(command) {
|
|
|
222
287
|
}
|
|
223
288
|
/** A trailing `| tail -N` / `| tail -n N` (stderr redirects before it stay). */
|
|
224
289
|
const TRAILING_TAIL = /\s*\|\s*tail(?:\s+-n)?\s+[-+]?\d+\s*$/;
|
|
290
|
+
/** `--reporter=line` / `--reporter line`, the one reporter the platform cannot read back. */
|
|
291
|
+
const LINE_REPORTER_FLAG = /--reporter([= ])line\b/;
|
|
225
292
|
/**
|
|
226
293
|
* Rewrite a command about to run in the background so its log grows while it
|
|
227
294
|
* runs. Two shapes left the log empty until exit, and an empty log is what
|
|
@@ -231,9 +298,16 @@ const TRAILING_TAIL = /\s*\|\s*tail(?:\s+-n)?\s+[-+]?\d+\s*$/;
|
|
|
231
298
|
* - a trailing `| tail -N`, which holds everything back until the command ends.
|
|
232
299
|
* It is dropped: `wait_for_task` already returns the end of the log.
|
|
233
300
|
* - `playwright test` with no `--reporter`, which runs the config's reporter —
|
|
234
|
-
* often `html`, which prints nothing per test. `--reporter=
|
|
301
|
+
* often `html`, which prints nothing per test. `--reporter=list` is added
|
|
235
302
|
* when the playwright run is the command's last step and nothing is piped or
|
|
236
|
-
* redirected from it
|
|
303
|
+
* redirected from it: it writes each result as it finishes, and it is the
|
|
304
|
+
* shape the platform reads a background suite back in (`suite-fail-fast.ts`'s
|
|
305
|
+
* fail-fast note and the Tests bar both parse `list` lines). A run that names
|
|
306
|
+
* `--reporter=line` is switched to `list` for the same reason — `line` writes
|
|
307
|
+
* `[n/N] <title>` with no ✓/✘ mark and no duration, so on X1 flash-m62 an
|
|
308
|
+
* 8.3-minute walk of one suite could never trip the fail-fast note.
|
|
309
|
+
*
|
|
310
|
+
* Only the command's last step is touched, and only its reporter.
|
|
237
311
|
*
|
|
238
312
|
* @param command - The command as the executor sent it.
|
|
239
313
|
* @returns The command to run and one sentence per change made (empty when none).
|
|
@@ -255,12 +329,20 @@ export function streamingBackgroundCommand(command) {
|
|
|
255
329
|
changes.push(`Dropped the trailing \`${head[0].trim()}\`: Playwright prints its pass/fail count last, and it would be cut.`);
|
|
256
330
|
}
|
|
257
331
|
const lastStep = out.split(/&&|\|\||;/).at(-1) ?? '';
|
|
258
|
-
|
|
259
|
-
!/--reporter\b/.test(lastStep) &&
|
|
332
|
+
const finalPlaywrightRun = /\bplaywright\s+test\b/.test(lastStep) &&
|
|
260
333
|
// `2>&1` only joins stderr to the log; any other pipe or redirect sends the output elsewhere.
|
|
261
|
-
!/[|>]/.test(lastStep.replace(/\d?>&\d/g, ''))
|
|
262
|
-
|
|
263
|
-
|
|
334
|
+
!/[|>]/.test(lastStep.replace(/\d?>&\d/g, ''));
|
|
335
|
+
if (finalPlaywrightRun && LINE_REPORTER_FLAG.test(lastStep)) {
|
|
336
|
+
// Only the last step is rewritten, and `lastStep` is a suffix of `out`.
|
|
337
|
+
out =
|
|
338
|
+
out.slice(0, out.length - lastStep.length) +
|
|
339
|
+
lastStep.replace(LINE_REPORTER_FLAG, '--reporter$1list');
|
|
340
|
+
changes.push('Switched `--reporter=line` to `--reporter=list`: it also writes each test result as it ' +
|
|
341
|
+
'finishes, and it is the shape the platform reads back from a background run.');
|
|
342
|
+
}
|
|
343
|
+
else if (finalPlaywrightRun && !/--reporter\b/.test(lastStep)) {
|
|
344
|
+
out = `${out.trimEnd()} --reporter=list`;
|
|
345
|
+
changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
|
|
264
346
|
'(the config reporter may print nothing until the end).');
|
|
265
347
|
}
|
|
266
348
|
return { command: out, changes };
|
|
@@ -279,7 +361,8 @@ export function streamingBackgroundCommand(command) {
|
|
|
279
361
|
* a keyless build still succeeds, and blocking those sent it looking for
|
|
280
362
|
* workarounds (X0 rehearsal 14).
|
|
281
363
|
*/
|
|
282
|
-
|
|
364
|
+
// `(?<!\\)`: a backslash-escaped `\|` is a literal in the shell (grep "a\|env"), not a pipe (X1 flash-m58).
|
|
365
|
+
const BLOCKED_COMMANDS = /(?<!\\)(?:^|[;&|`]\s*|(?:sh|bash|zsh|dash)\s+-c\s+['"]?\s*)(?:\/usr\/bin\/)?(?:\benv(?:\s+(?:-0|--null|--))?\s*(?:$|[;&|>)`'"])|\bprintenv\b|\bexport\s*$|\bset\s*$|\bdeclare\s+-x|cat\s+\/etc\/environment|cat\s+\/root\/\.bashrc|cat\s+\/proc\/\d+\/environ|cat\s+\/proc\/self\/environ|strings\s+\/proc|xargs[^;&|\n]*\/proc\/[^;&|\n]*environ|less\s+\/proc|head\s+\/proc|tail\s+\/proc|xxd\s+\/proc|od\s+\/proc|base64\s+\/proc|dd\s[^\n]*\/proc|sed\s[^\n]*\/proc\/[^\n]*environ|awk\s[^\n]*\/proc\/[^\n]*environ|cp\s[^\n]*\/proc\/[^\n]*environ|(?:cat|less|head|tail|xxd|od|base64|strings|dd|cp|sed|awk)\s[^\n;&|]*\/etc\/mol(?:\/|\b))/i;
|
|
283
366
|
/** Block shell redirects from /proc environ. */
|
|
284
367
|
const BLOCKED_PROC_REDIRECT = /(?:<\s*\/proc\/(?:\d+|self)\/environ)/i;
|
|
285
368
|
/** Block shell redirects from the molecule platform secrets dir (`/etc/mol`). */
|
|
@@ -550,6 +633,33 @@ export function truncate(s, maxLength) {
|
|
|
550
633
|
export function truncateMiddle(s, maxLength) {
|
|
551
634
|
if (s.length <= maxLength)
|
|
552
635
|
return s;
|
|
636
|
+
// Room for the tally line it may append, so the result stays near the cap.
|
|
637
|
+
return withRunTally(s, truncateMiddleBody(s, Math.max(Math.floor(maxLength / 2), maxLength - 200)));
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* A test run's tally lines (` 3 failed`, ` 71 passed (4.6m)`) the cut text lost, put back at
|
|
641
|
+
* its end: Playwright lists every failed test after its "N failed" line, so a long list pushed
|
|
642
|
+
* the line itself into the cut middle and the run read as having no failures (X1 flash-m60).
|
|
643
|
+
*
|
|
644
|
+
* @param original - The whole output.
|
|
645
|
+
* @param cut - The truncated output.
|
|
646
|
+
* @returns The cut output, with any lost tally lines appended.
|
|
647
|
+
*/
|
|
648
|
+
function withRunTally(original, cut) {
|
|
649
|
+
const tally = [
|
|
650
|
+
...original.matchAll(/^\s*\d+ (?:failed|passed|flaky|skipped|did not run|interrupted)\b[^\n]{0,40}$/gm),
|
|
651
|
+
].map((m) => m[0].trim());
|
|
652
|
+
const lost = tally.filter((line) => !cut.includes(line));
|
|
653
|
+
return lost.length ? `${cut}\n[run tally] ${lost.join(' · ')}` : cut;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* {@link truncateMiddle} without the tally guard.
|
|
657
|
+
*
|
|
658
|
+
* @param s - The text.
|
|
659
|
+
* @param maxLength - The budget.
|
|
660
|
+
* @returns The cut text.
|
|
661
|
+
*/
|
|
662
|
+
function truncateMiddleBody(s, maxLength) {
|
|
553
663
|
// Reserve room for the elision notice so head + notice + tail stays WITHIN
|
|
554
664
|
// maxLength — the cap is a real token budget the caller relies on (a head-only
|
|
555
665
|
// truncate that overshoots by its suffix length breaks that contract). 140 =
|
|
@@ -620,6 +730,33 @@ export function failureBlocks(s) {
|
|
|
620
730
|
blocks.push(current.join('\n').trimEnd());
|
|
621
731
|
return blocks;
|
|
622
732
|
}
|
|
733
|
+
/** Failure lines shown from a log that is still being written. */
|
|
734
|
+
export const RUNNING_FAILURE_LINES = 40;
|
|
735
|
+
/** Bytes of a running log's end appended after its failure lines. */
|
|
736
|
+
export const RUNNING_TAIL_BYTES = 1000;
|
|
737
|
+
/**
|
|
738
|
+
* The lines worth showing from a log that is still being written: a failed test's mark or
|
|
739
|
+
* numbered block header, and a runner's own error lines. Matched with `grep -E`.
|
|
740
|
+
*/
|
|
741
|
+
export const RUNNING_FAILURE_LINE_RE = '^[[:space:]]*(✘|✗|×|✖|✕|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)';
|
|
742
|
+
/**
|
|
743
|
+
* The shell command whose stdout is the interesting part of a log that is still being written:
|
|
744
|
+
* the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
|
|
745
|
+
*
|
|
746
|
+
* A Playwright `list` run prints one line per test as it finishes and its error blocks only at
|
|
747
|
+
* the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
|
|
748
|
+
* the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
|
|
749
|
+
* them, which cost the run the grep it had to make anyway.
|
|
750
|
+
*
|
|
751
|
+
* @param log - The task log path.
|
|
752
|
+
* @param own - A file the command redirects its own output to, if any.
|
|
753
|
+
* @returns The shell command, safe to hand to the execution backend as-is.
|
|
754
|
+
*/
|
|
755
|
+
export function runningOutputSliceCommand(log, own) {
|
|
756
|
+
const read = `cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null`;
|
|
757
|
+
return (`{ ${read} | grep -aE ${shellQuote(RUNNING_FAILURE_LINE_RE)} | head -${RUNNING_FAILURE_LINES} | cut -c1-300; ` +
|
|
758
|
+
`echo; ${read} | tail -c ${RUNNING_TAIL_BYTES}; }`);
|
|
759
|
+
}
|
|
623
760
|
/**
|
|
624
761
|
* Attempt a whitespace-tolerant replacement when an exact `old_string` match
|
|
625
762
|
* failed. Finds a contiguous run of lines in `content` whose per-line
|
|
@@ -720,3 +857,33 @@ export function parseCheckProblem(result) {
|
|
|
720
857
|
return null;
|
|
721
858
|
return text.slice(0, 1200);
|
|
722
859
|
}
|
|
860
|
+
/**
|
|
861
|
+
* The line a replacement fused when it dropped the newline its `old_string`
|
|
862
|
+
* ended with: `new_string`'s text is followed on the same line by what used to
|
|
863
|
+
* be the next line. Read off the file as it now is, so the line shown is the
|
|
864
|
+
* line the parser is actually complaining about.
|
|
865
|
+
*
|
|
866
|
+
* @param content - The file's content after the edit.
|
|
867
|
+
* @param newString - A replacement's `new_string`, whose `old_string` ended with a newline.
|
|
868
|
+
* @returns The fused line's 1-based number and its text, or null when the
|
|
869
|
+
* replacement's text is not followed by more of the same line (it ends the
|
|
870
|
+
* file, the following newline survived, or another replacement rewrote it).
|
|
871
|
+
*/
|
|
872
|
+
export function fusedLine(content, newString) {
|
|
873
|
+
for (let at = content.indexOf(newString); at >= 0; at = content.indexOf(newString, at + 1)) {
|
|
874
|
+
const endsAt = at + newString.length;
|
|
875
|
+
const next = content[endsAt];
|
|
876
|
+
// End of file (nothing was joined) or a newline (the boundary survived) are
|
|
877
|
+
// both "not fused". A later replacement in the same batch may have rewritten
|
|
878
|
+
// this text, so keep looking rather than giving up on the first occurrence.
|
|
879
|
+
if (next === undefined || next === '\n')
|
|
880
|
+
continue;
|
|
881
|
+
const lineStart = content.lastIndexOf('\n', at) + 1;
|
|
882
|
+
const lineEnd = content.indexOf('\n', endsAt);
|
|
883
|
+
return {
|
|
884
|
+
line: content.slice(0, lineStart).split('\n').length,
|
|
885
|
+
text: content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd),
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
}
|