@molecule/api-ai-tools 1.5.19 → 1.5.20
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 +43 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/no-test-run.d.ts +45 -0
- package/dist/no-test-run.d.ts.map +1 -0
- package/dist/no-test-run.js +125 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +6 -3
- package/dist/utilities.d.ts +19 -1
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +34 -8
- 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-16T12:
|
|
6
|
+
Generated: 2026-10-16T12:46:49.316Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -499,6 +499,27 @@ function guardAITool(tool: T, options?: GuardOptions): T
|
|
|
499
499
|
|
|
500
500
|
**Returns:** A copy with a guarded `execute`.
|
|
501
501
|
|
|
502
|
+
#### `commandRunsNoTests(command)`
|
|
503
|
+
|
|
504
|
+
Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
|
|
505
|
+
command runs no test at all.
|
|
506
|
+
|
|
507
|
+
A command that invokes no runner is NOT one of these (false): "the command runs no test" is
|
|
508
|
+
then true for `ls`, which is not the question any caller is asking. The question is whether
|
|
509
|
+
the runner invocation each caller read as a run is one.
|
|
510
|
+
|
|
511
|
+
A command that also runs tests is false however it is spelled: `npx playwright install &&
|
|
512
|
+
npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
|
|
513
|
+
reading a real run.
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
function commandRunsNoTests(command: string): boolean
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
- `command` — The shell command as the executor sent it.
|
|
520
|
+
|
|
521
|
+
**Returns:** True when the command invokes a runner only to print.
|
|
522
|
+
|
|
502
523
|
#### `discoverSkills(backend)`
|
|
503
524
|
|
|
504
525
|
Discover skills from a project directory.
|
|
@@ -758,6 +779,10 @@ function streamingBackgroundCommand(command: string): { command: string; changes
|
|
|
758
779
|
Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run
|
|
759
780
|
— rather than a build, an install or a one-off script.
|
|
760
781
|
|
|
782
|
+
A command that invokes a runner only to print is not one of these
|
|
783
|
+
({@link commandRunsNoTests}): `npx playwright test --list` prints the tests it would run and
|
|
784
|
+
exits, and `npx playwright install`, `vitest list` and `hurl --help` start no test either.
|
|
785
|
+
|
|
761
786
|
```typescript
|
|
762
787
|
function isSuiteRunCommand(command: string): boolean
|
|
763
788
|
```
|
|
@@ -766,6 +791,23 @@ function isSuiteRunCommand(command: string): boolean
|
|
|
766
791
|
|
|
767
792
|
**Returns:** True when the command starts one of those runners.
|
|
768
793
|
|
|
794
|
+
#### `isPlaywrightRunCommand(command)`
|
|
795
|
+
|
|
796
|
+
Whether a command starts a Playwright run — `playwright test` in a form that runs something.
|
|
797
|
+
|
|
798
|
+
`npx playwright test --list` matched every `\bplaywright\s+test\b` test in the platform and was
|
|
799
|
+
then treated as a run: held (flash-m79's config check), backgrounded for a run that outlasts
|
|
800
|
+
the tool's ceiling, and rewritten with `--reporter`. A list takes two seconds, prints its lines
|
|
801
|
+
and exits, so it is none of those; {@link commandRunsNoTests} is what decides.
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
function isPlaywrightRunCommand(command: string): boolean
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
- `command` — The shell command.
|
|
808
|
+
|
|
809
|
+
**Returns:** True when the command starts a Playwright run.
|
|
810
|
+
|
|
769
811
|
#### `suiteRunPriorityLaunch(command, launch)`
|
|
770
812
|
|
|
771
813
|
Start a background suite run at lower CPU and IO priority, so the sandbox it
|
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ export * from './backends/local.js';
|
|
|
117
117
|
export * from './backends/sandbox.js';
|
|
118
118
|
export * from './browser-guard.js';
|
|
119
119
|
export * from './input-normalizer.js';
|
|
120
|
+
export * from './no-test-run.js';
|
|
120
121
|
export * from './schemas.js';
|
|
121
122
|
export * from './system-prompt.js';
|
|
122
123
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
|
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,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,7 @@ export * from './backends/local.js';
|
|
|
117
117
|
export * from './backends/sandbox.js';
|
|
118
118
|
export * from './browser-guard.js';
|
|
119
119
|
export * from './input-normalizer.js';
|
|
120
|
+
export * from './no-test-run.js';
|
|
120
121
|
export * from './schemas.js';
|
|
121
122
|
export * from './system-prompt.js';
|
|
122
123
|
export * from './tools.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner invocations that print information and run no test — the one classifier every
|
|
3
|
+
* site asks before it treats a command as a test run.
|
|
4
|
+
*
|
|
5
|
+
* A Playwright CLI has subcommands and flags that never start a test: `playwright test --list`
|
|
6
|
+
* prints the tests it would run and exits, `show-report`/`show-trace` open a saved report,
|
|
7
|
+
* `codegen` records one, `install` fetches browsers, `merge-reports` joins blobs, and
|
|
8
|
+
* `--help`/`--version` print and exit. Vitest lists with `vitest list` / `--list` and hurl
|
|
9
|
+
* prints with `--help`/`--version`, the same way.
|
|
10
|
+
*
|
|
11
|
+
* Every check in the platform read such a command as a whole-suite run, because each one
|
|
12
|
+
* matched the runner's NAME. X1 xr-molecule-flash-m79's first Playwright command was a config
|
|
13
|
+
* check — a config written to `/tmp`, `cp`'d beside the suite and used with `--list` — and it
|
|
14
|
+
* came back HELD, `status: not_run`, with a ~4 KB `suiteNote` telling it to send the same
|
|
15
|
+
* command with `--max-failures=15` ("uncapped it walks every test … 8–13 minutes"), beside an
|
|
16
|
+
* environment block saying the run "ran on their defaults" and an import note about a path the
|
|
17
|
+
* `cp` had already fixed. Nothing ran. The model gave up on the approach, wrote a second
|
|
18
|
+
* config, and its next command was held again (flash-m74's "same command three times" shape):
|
|
19
|
+
* two holds, zero tests, and the config check it asked for never answered.
|
|
20
|
+
*
|
|
21
|
+
* So the classifiers live here, in one place, and callers ask them instead of their own
|
|
22
|
+
* regexes: {@link commandRunsNoTests} answers for a whole command, and the runner-specific
|
|
23
|
+
* sites (molecule-dev's suite checks, `streamingBackgroundCommand`, `isSuiteRunCommand`) route
|
|
24
|
+
* through it. A command that runs no test is never held, never noted, and never rewritten
|
|
25
|
+
* (`--retries`, `--reporter`) as if it were a run.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
|
|
31
|
+
* command runs no test at all.
|
|
32
|
+
*
|
|
33
|
+
* A command that invokes no runner is NOT one of these (false): "the command runs no test" is
|
|
34
|
+
* then true for `ls`, which is not the question any caller is asking. The question is whether
|
|
35
|
+
* the runner invocation each caller read as a run is one.
|
|
36
|
+
*
|
|
37
|
+
* A command that also runs tests is false however it is spelled: `npx playwright install &&
|
|
38
|
+
* npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
|
|
39
|
+
* reading a real run.
|
|
40
|
+
*
|
|
41
|
+
* @param command - The shell command as the executor sent it.
|
|
42
|
+
* @returns True when the command invokes a runner only to print.
|
|
43
|
+
*/
|
|
44
|
+
export declare function commandRunsNoTests(command: string): boolean;
|
|
45
|
+
//# sourceMappingURL=no-test-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-test-run.d.ts","sourceRoot":"","sources":["../src/no-test-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA6EH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAY3D"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner invocations that print information and run no test — the one classifier every
|
|
3
|
+
* site asks before it treats a command as a test run.
|
|
4
|
+
*
|
|
5
|
+
* A Playwright CLI has subcommands and flags that never start a test: `playwright test --list`
|
|
6
|
+
* prints the tests it would run and exits, `show-report`/`show-trace` open a saved report,
|
|
7
|
+
* `codegen` records one, `install` fetches browsers, `merge-reports` joins blobs, and
|
|
8
|
+
* `--help`/`--version` print and exit. Vitest lists with `vitest list` / `--list` and hurl
|
|
9
|
+
* prints with `--help`/`--version`, the same way.
|
|
10
|
+
*
|
|
11
|
+
* Every check in the platform read such a command as a whole-suite run, because each one
|
|
12
|
+
* matched the runner's NAME. X1 xr-molecule-flash-m79's first Playwright command was a config
|
|
13
|
+
* check — a config written to `/tmp`, `cp`'d beside the suite and used with `--list` — and it
|
|
14
|
+
* came back HELD, `status: not_run`, with a ~4 KB `suiteNote` telling it to send the same
|
|
15
|
+
* command with `--max-failures=15` ("uncapped it walks every test … 8–13 minutes"), beside an
|
|
16
|
+
* environment block saying the run "ran on their defaults" and an import note about a path the
|
|
17
|
+
* `cp` had already fixed. Nothing ran. The model gave up on the approach, wrote a second
|
|
18
|
+
* config, and its next command was held again (flash-m74's "same command three times" shape):
|
|
19
|
+
* two holds, zero tests, and the config check it asked for never answered.
|
|
20
|
+
*
|
|
21
|
+
* So the classifiers live here, in one place, and callers ask them instead of their own
|
|
22
|
+
* regexes: {@link commandRunsNoTests} answers for a whole command, and the runner-specific
|
|
23
|
+
* sites (molecule-dev's suite checks, `streamingBackgroundCommand`, `isSuiteRunCommand`) route
|
|
24
|
+
* through it. A command that runs no test is never held, never noted, and never rewritten
|
|
25
|
+
* (`--retries`, `--reporter`) as if it were a run.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* A runner's name as a statement invokes it, from the start of its own arguments. The trailing
|
|
31
|
+
* guard is what separates the runner from a FILE named after it — `-c playwright.local.config.ts`
|
|
32
|
+
* names a config, not a second invocation, and reading it as one would give that "invocation" no
|
|
33
|
+
* `--list` of its own and un-do the answer for the real one.
|
|
34
|
+
*/
|
|
35
|
+
const INVOCATIONS = [
|
|
36
|
+
['playwright', /(?:^|[\s;&|(])(?:npx\s+(?:--no-install\s+)?)?(?:\S*\/)?playwright(?![\w.-])/g],
|
|
37
|
+
['vitest', /(?:^|[\s;&|(])(?:npx\s+(?:--no-install\s+)?)?(?:\S*\/)?vitest(?![\w.-])/g],
|
|
38
|
+
['hurl', /(?:^|[\s;&|(])(?:\S*\/)?hurl(?![\w.-])/g],
|
|
39
|
+
];
|
|
40
|
+
/** Playwright's subcommands that start no test. */
|
|
41
|
+
const PLAYWRIGHT_INFO_SUBCOMMANDS = [
|
|
42
|
+
'show-report',
|
|
43
|
+
'show-trace',
|
|
44
|
+
'codegen',
|
|
45
|
+
'install',
|
|
46
|
+
'merge-reports',
|
|
47
|
+
];
|
|
48
|
+
/** Vitest's subcommand that prints what a run would collect. */
|
|
49
|
+
const VITEST_LIST_SUBCOMMAND = 'list';
|
|
50
|
+
/**
|
|
51
|
+
* The flags that put a runner in an information mode: it prints and exits. `-h` is `--help`
|
|
52
|
+
* for all three (hurl's manual documents `-h, --help`; Playwright's and Vitest's CLIs follow
|
|
53
|
+
* the same convention), and the value-taking flags beside them are why each is matched as a
|
|
54
|
+
* whole token — hurl's `-H` is a header, not help.
|
|
55
|
+
*/
|
|
56
|
+
const NO_RUN_FLAGS = {
|
|
57
|
+
playwright: [
|
|
58
|
+
/(?:^|\s)--list(?=\s|$)/,
|
|
59
|
+
/(?:^|\s)--dry-run(?=\s|$)/,
|
|
60
|
+
/(?:^|\s)--help(?=\s|$)/,
|
|
61
|
+
/(?:^|\s)-h(?=\s|$)/,
|
|
62
|
+
/(?:^|\s)--version(?=\s|$)/,
|
|
63
|
+
],
|
|
64
|
+
vitest: [
|
|
65
|
+
/(?:^|\s)--list(?=\s|$)/,
|
|
66
|
+
/(?:^|\s)--help(?=\s|$)/,
|
|
67
|
+
/(?:^|\s)-h(?=\s|$)/,
|
|
68
|
+
/(?:^|\s)--version(?=\s|$)/,
|
|
69
|
+
],
|
|
70
|
+
hurl: [/(?:^|\s)--help(?=\s|$)/, /(?:^|\s)-h(?=\s|$)/, /(?:^|\s)--version(?=\s|$)/],
|
|
71
|
+
};
|
|
72
|
+
/** Statements of a shell command, the way the suite checks split them. */
|
|
73
|
+
const STATEMENT_SEP = /\s*(?:&&|\|\||;|\||\n)\s*/;
|
|
74
|
+
/**
|
|
75
|
+
* A heredoc body is data a command writes, not what it runs: a `playwright test --list` inside
|
|
76
|
+
* the config a command writes is not a run of this command either way. The pattern consumes the
|
|
77
|
+
* writer's own line prefix, the same shape `outputRedirectTarget` reads.
|
|
78
|
+
*/
|
|
79
|
+
const HEREDOC = /[^\n;&|]*<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g;
|
|
80
|
+
/**
|
|
81
|
+
* Whether this runner's invocation — the text from just after its name to the end of its
|
|
82
|
+
* statement — only prints.
|
|
83
|
+
*
|
|
84
|
+
* @param runner - The runner the statement invokes.
|
|
85
|
+
* @param after - Everything after the runner's name in that statement.
|
|
86
|
+
* @returns True when the invocation starts no test.
|
|
87
|
+
*/
|
|
88
|
+
function informational(runner, after) {
|
|
89
|
+
const first = after.trimStart().split(/\s+/)[0] ?? '';
|
|
90
|
+
if (runner === 'playwright' && PLAYWRIGHT_INFO_SUBCOMMANDS.includes(first))
|
|
91
|
+
return true;
|
|
92
|
+
if (runner === 'vitest' && first === VITEST_LIST_SUBCOMMAND)
|
|
93
|
+
return true;
|
|
94
|
+
return NO_RUN_FLAGS[runner].some((re) => re.test(after));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
|
|
98
|
+
* command runs no test at all.
|
|
99
|
+
*
|
|
100
|
+
* A command that invokes no runner is NOT one of these (false): "the command runs no test" is
|
|
101
|
+
* then true for `ls`, which is not the question any caller is asking. The question is whether
|
|
102
|
+
* the runner invocation each caller read as a run is one.
|
|
103
|
+
*
|
|
104
|
+
* A command that also runs tests is false however it is spelled: `npx playwright install &&
|
|
105
|
+
* npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
|
|
106
|
+
* reading a real run.
|
|
107
|
+
*
|
|
108
|
+
* @param command - The shell command as the executor sent it.
|
|
109
|
+
* @returns True when the command invokes a runner only to print.
|
|
110
|
+
*/
|
|
111
|
+
export function commandRunsNoTests(command) {
|
|
112
|
+
if (typeof command !== 'string' || !command.trim())
|
|
113
|
+
return false;
|
|
114
|
+
let invocations = 0;
|
|
115
|
+
for (const statement of command.replace(HEREDOC, ' ').split(STATEMENT_SEP)) {
|
|
116
|
+
for (const [runner, re] of INVOCATIONS) {
|
|
117
|
+
for (const m of statement.matchAll(re)) {
|
|
118
|
+
invocations += 1;
|
|
119
|
+
if (!informational(runner, statement.slice((m.index ?? 0) + m[0].length)))
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return invocations > 0;
|
|
125
|
+
}
|
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;AAkL9C,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;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAqDnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA0zDxF"}
|
package/dist/tools.js
CHANGED
|
@@ -161,7 +161,7 @@ function waitProgressNote(o) {
|
|
|
161
161
|
const sentence = parts.join('; ');
|
|
162
162
|
return ` ${sentence[0].toUpperCase()}${sentence.slice(1)}.`;
|
|
163
163
|
}
|
|
164
|
-
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, findPatternArgs, fusedLine, htmlOutline, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, 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, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
|
|
164
|
+
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, findPatternArgs, fusedLine, htmlOutline, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, 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, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
|
|
165
165
|
/**
|
|
166
166
|
* How long exec_command waits for a command it backgrounded only because the
|
|
167
167
|
* command asked for more than the tool's ceiling, before handing back a task id.
|
|
@@ -1371,12 +1371,15 @@ export function buildTools(backend, config) {
|
|
|
1371
1371
|
// declaring a budget too: a suite routinely outlasts the ceiling, and killed there
|
|
1372
1372
|
// behind `| tail` it returns nothing (X1 flash-m29 lost a 290 s run that way).
|
|
1373
1373
|
// A short one still comes back as a foreground result, through the grace below.
|
|
1374
|
+
// A Playwright command that runs no test is not one of these either: `playwright test
|
|
1375
|
+
// --list` piped through `tail` takes two seconds, and backgrounding it costs the round
|
|
1376
|
+
// trip `wait_for_task` owes the executor (flash-m79's config check).
|
|
1374
1377
|
const overCeiling = commandBudgetMs !== undefined &&
|
|
1375
1378
|
((outputIsWithheldUntilExit(command) &&
|
|
1376
|
-
(declaredSeconds > ceilingSeconds ||
|
|
1379
|
+
(declaredSeconds > ceilingSeconds || isPlaywrightRunCommand(command))) ||
|
|
1377
1380
|
// A redirected Playwright run shows nothing either (`> out.txt`), and was killed at
|
|
1378
1381
|
// the ceiling with 38 tests unrun (X1 flash-m36).
|
|
1379
|
-
(
|
|
1382
|
+
(isPlaywrightRunCommand(command) && outputRedirectTarget(command) !== null));
|
|
1380
1383
|
if (input.run_in_background === true || overCeiling) {
|
|
1381
1384
|
const id = `mol-bg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1382
1385
|
const log = `/tmp/${id}.log`;
|
package/dist/utilities.d.ts
CHANGED
|
@@ -136,7 +136,9 @@ export declare function outputRedirectTarget(command: string): string | null;
|
|
|
136
136
|
* its error message. X1 flash-m76 walked 15 tests for 4.0 minutes, every one a
|
|
137
137
|
* `TimeoutError: page.waitForURL` from one missing `navigate('/')`.
|
|
138
138
|
*
|
|
139
|
-
* Only the command's last step is touched, and only its reporter
|
|
139
|
+
* Only the command's last step is touched, and only its reporter, and only when that step runs
|
|
140
|
+
* something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
141
|
+
* neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}).
|
|
140
142
|
*
|
|
141
143
|
* @param command - The command as the executor sent it.
|
|
142
144
|
* @returns The command to run and one sentence per change made (empty when none).
|
|
@@ -149,10 +151,26 @@ export declare function streamingBackgroundCommand(command: string): {
|
|
|
149
151
|
* Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run
|
|
150
152
|
* — rather than a build, an install or a one-off script.
|
|
151
153
|
*
|
|
154
|
+
* A command that invokes a runner only to print is not one of these
|
|
155
|
+
* ({@link commandRunsNoTests}): `npx playwright test --list` prints the tests it would run and
|
|
156
|
+
* exits, and `npx playwright install`, `vitest list` and `hurl --help` start no test either.
|
|
157
|
+
*
|
|
152
158
|
* @param command - The shell command.
|
|
153
159
|
* @returns True when the command starts one of those runners.
|
|
154
160
|
*/
|
|
155
161
|
export declare function isSuiteRunCommand(command: string): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Whether a command starts a Playwright run — `playwright test` in a form that runs something.
|
|
164
|
+
*
|
|
165
|
+
* `npx playwright test --list` matched every `\bplaywright\s+test\b` test in the platform and was
|
|
166
|
+
* then treated as a run: held (flash-m79's config check), backgrounded for a run that outlasts
|
|
167
|
+
* the tool's ceiling, and rewritten with `--reporter`. A list takes two seconds, prints its lines
|
|
168
|
+
* and exits, so it is none of those; {@link commandRunsNoTests} is what decides.
|
|
169
|
+
*
|
|
170
|
+
* @param command - The shell command.
|
|
171
|
+
* @returns True when the command starts a Playwright run.
|
|
172
|
+
*/
|
|
173
|
+
export declare function isPlaywrightRunCommand(command: string): boolean;
|
|
156
174
|
/**
|
|
157
175
|
* Start a background suite run at lower CPU and IO priority, so the sandbox it
|
|
158
176
|
* shares with everything else stays responsive to what the executor does next.
|
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;
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
|
package/dist/utilities.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { posix } from 'path';
|
|
8
|
+
import { commandRunsNoTests } from './no-test-run.js';
|
|
8
9
|
/**
|
|
9
10
|
* Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
|
|
10
11
|
* single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
|
|
@@ -315,12 +316,19 @@ const LINE_REPORTER_FLAG = /--reporter([= ])line\b/;
|
|
|
315
316
|
* its error message. X1 flash-m76 walked 15 tests for 4.0 minutes, every one a
|
|
316
317
|
* `TimeoutError: page.waitForURL` from one missing `navigate('/')`.
|
|
317
318
|
*
|
|
318
|
-
* Only the command's last step is touched, and only its reporter
|
|
319
|
+
* Only the command's last step is touched, and only its reporter, and only when that step runs
|
|
320
|
+
* something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
321
|
+
* neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}).
|
|
319
322
|
*
|
|
320
323
|
* @param command - The command as the executor sent it.
|
|
321
324
|
* @returns The command to run and one sentence per change made (empty when none).
|
|
322
325
|
*/
|
|
323
326
|
export function streamingBackgroundCommand(command) {
|
|
327
|
+
// A command that runs no test is not a run to keep a growing log for: nothing about it is
|
|
328
|
+
// rewritten (flash-m79's `--list` was read as a whole-suite run end to end, and got a ~4 KB
|
|
329
|
+
// refusal to send itself again with `--max-failures=15`).
|
|
330
|
+
if (commandRunsNoTests(command))
|
|
331
|
+
return { command, changes: [] };
|
|
324
332
|
const changes = [];
|
|
325
333
|
let out = command;
|
|
326
334
|
const tail = TRAILING_TAIL.exec(out);
|
|
@@ -332,12 +340,12 @@ export function streamingBackgroundCommand(command) {
|
|
|
332
340
|
// A Playwright run behind `| head -N` loses its tally, which prints last (X1
|
|
333
341
|
// flash-m32 counted ✓/✘ marks by hand); the full log comes back anyway.
|
|
334
342
|
const head = /\s*\|\s*head(?:\s+-n)?\s+-?\d+\s*$/.exec(out);
|
|
335
|
-
if (head &&
|
|
343
|
+
if (head && isPlaywrightRunCommand(out)) {
|
|
336
344
|
out = out.slice(0, head.index);
|
|
337
345
|
changes.push(`Dropped the trailing \`${head[0].trim()}\`: Playwright prints its pass/fail count last, and it would be cut.`);
|
|
338
346
|
}
|
|
339
347
|
const lastStep = out.split(/&&|\|\||;/).at(-1) ?? '';
|
|
340
|
-
const finalPlaywrightRun =
|
|
348
|
+
const finalPlaywrightRun = isPlaywrightRunCommand(lastStep) &&
|
|
341
349
|
// `2>&1` only joins stderr to the log; any other pipe or redirect sends the output elsewhere.
|
|
342
350
|
!/[|>]/.test(lastStep.replace(/\d?>&\d/g, ''));
|
|
343
351
|
// A diagnosis or a re-run (capped, last-failed, grepped, or named files) walks each failure
|
|
@@ -405,11 +413,29 @@ const SUITE_RUN_COMMANDS = [
|
|
|
405
413
|
* Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run
|
|
406
414
|
* — rather than a build, an install or a one-off script.
|
|
407
415
|
*
|
|
416
|
+
* A command that invokes a runner only to print is not one of these
|
|
417
|
+
* ({@link commandRunsNoTests}): `npx playwright test --list` prints the tests it would run and
|
|
418
|
+
* exits, and `npx playwright install`, `vitest list` and `hurl --help` start no test either.
|
|
419
|
+
*
|
|
408
420
|
* @param command - The shell command.
|
|
409
421
|
* @returns True when the command starts one of those runners.
|
|
410
422
|
*/
|
|
411
423
|
export function isSuiteRunCommand(command) {
|
|
412
|
-
return SUITE_RUN_COMMANDS.some((re) => re.test(command));
|
|
424
|
+
return !commandRunsNoTests(command) && SUITE_RUN_COMMANDS.some((re) => re.test(command));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Whether a command starts a Playwright run — `playwright test` in a form that runs something.
|
|
428
|
+
*
|
|
429
|
+
* `npx playwright test --list` matched every `\bplaywright\s+test\b` test in the platform and was
|
|
430
|
+
* then treated as a run: held (flash-m79's config check), backgrounded for a run that outlasts
|
|
431
|
+
* the tool's ceiling, and rewritten with `--reporter`. A list takes two seconds, prints its lines
|
|
432
|
+
* and exits, so it is none of those; {@link commandRunsNoTests} is what decides.
|
|
433
|
+
*
|
|
434
|
+
* @param command - The shell command.
|
|
435
|
+
* @returns True when the command starts a Playwright run.
|
|
436
|
+
*/
|
|
437
|
+
export function isPlaywrightRunCommand(command) {
|
|
438
|
+
return /\bplaywright\s+test\b/.test(command) && !commandRunsNoTests(command);
|
|
413
439
|
}
|
|
414
440
|
/**
|
|
415
441
|
* Start a background suite run at lower CPU and IO priority, so the sandbox it
|
|
@@ -792,6 +818,8 @@ export function looksLikeHtml(text) {
|
|
|
792
818
|
export function htmlOutline(text, label = 'document') {
|
|
793
819
|
if (!looksLikeHtml(text))
|
|
794
820
|
return null;
|
|
821
|
+
// "0 KB" for anything under a kilobyte reads like a bug; say the real unit.
|
|
822
|
+
const size = (chars) => chars >= 1024 ? `${Math.round(chars / 1024)} KB` : `${chars} char${chars === 1 ? '' : 's'}`;
|
|
795
823
|
const lines = text.split('\n');
|
|
796
824
|
const out = [];
|
|
797
825
|
let skipped = 0;
|
|
@@ -800,7 +828,7 @@ export function htmlOutline(text, label = 'document') {
|
|
|
800
828
|
const flushSkipped = () => {
|
|
801
829
|
if (skipped === 0)
|
|
802
830
|
return;
|
|
803
|
-
out.push(` … ${skipped} more line${skipped === 1 ? '' : 's'} (${
|
|
831
|
+
out.push(` … ${skipped} more line${skipped === 1 ? '' : 's'} (${size(skippedChars)}) …`);
|
|
804
832
|
skipped = 0;
|
|
805
833
|
skippedChars = 0;
|
|
806
834
|
};
|
|
@@ -824,9 +852,7 @@ export function htmlOutline(text, label = 'document') {
|
|
|
824
852
|
}
|
|
825
853
|
}
|
|
826
854
|
flushSkipped();
|
|
827
|
-
|
|
828
|
-
const size = text.length >= 1024 ? `${Math.round(text.length / 1024)} KB` : `${text.length} chars`;
|
|
829
|
-
const header = `OUTLINE of ${label} — ${size}, ${lines.length} lines. ` +
|
|
855
|
+
const header = `OUTLINE of ${label} — ${size(text.length)}, ${lines.length} lines. ` +
|
|
830
856
|
`Its tags and the tags that open each section, one line each; tag bodies (CSS, JS, text) are not shown.` +
|
|
831
857
|
(truncated ? ` (Outline capped at ${MAX_HTML_OUTLINE_LINES} lines.)` : '');
|
|
832
858
|
return `${header}\n\n${out.join('\n')}`;
|