testronaut 1.3.4 → 1.3.6
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/.devcontainer/devcontainer.json +21 -3
- package/.devcontainer/setup-github-auth.sh +36 -0
- package/.github/workflows/prepare-release.yml +168 -0
- package/.github/workflows/publish.yml +133 -0
- package/bin/cli.js +67 -0
- package/bin/init.js +1 -0
- package/bin/initHelpers.js +4 -0
- package/core/agent.js +13 -1
- package/core/config.js +65 -0
- package/core/redaction.js +30 -7
- package/core/turnLoop.js +35 -4
- package/package.json +5 -1
- package/runner/testronaut.js +15 -2
- package/tests/binTests/init.test.js +6 -0
- package/tests/binTests/initHelpersTests/defaultConfig.test.js +2 -0
- package/tests/coreTests/config.test.js +40 -0
- package/tests/coreTests/redation.test.js +30 -0
- package/tests/coreTests/turnLoop.test.js +1 -0
- package/tests/runnerTests/testronaut.test.js +7 -5
- package/tests/toolsTests/humanInput.test.js +26 -0
- package/tests/toolsTests/toolSchema.test.js +8 -0
- package/tests/toolsTests/turnLoopUtils.test.js +24 -0
- package/tools/chromeBrowser.js +2 -0
- package/tools/generateHtmlReport.js +13 -4
- package/tools/humanInput.js +165 -0
- package/tools/toolSchema.js +28 -0
- package/tools/turnLoopUtils.js +10 -6
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
|
|
5
5
|
|
|
6
|
+
"features": {
|
|
7
|
+
"ghcr.io/devcontainers/features/github-cli:1": {}
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
"secrets": {
|
|
11
|
+
"GH_TOKEN": {
|
|
12
|
+
"description": "A GitHub PAT with read access to mission-testronaut/testronaut-demo."
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"containerEnv": {
|
|
17
|
+
"GH_TOKEN": "${localEnv:GH_TOKEN}"
|
|
18
|
+
},
|
|
19
|
+
|
|
6
20
|
"customizations": {
|
|
7
21
|
"codespaces": {
|
|
8
22
|
"repositories": {
|
|
@@ -11,6 +25,9 @@
|
|
|
11
25
|
},
|
|
12
26
|
"mission-testronaut/testronaut-examples": {
|
|
13
27
|
"permissions": "write-all"
|
|
28
|
+
},
|
|
29
|
+
"mission-testronaut/testronaut-demo": {
|
|
30
|
+
"permissions": "write-all"
|
|
14
31
|
}
|
|
15
32
|
}
|
|
16
33
|
},
|
|
@@ -29,16 +46,17 @@
|
|
|
29
46
|
"eamodio.gitlens",
|
|
30
47
|
"esbenp.prettier-vscode",
|
|
31
48
|
"dbaeumer.vscode-eslint",
|
|
32
|
-
"openai.chatgpt"
|
|
49
|
+
"openai.chatgpt",
|
|
50
|
+
"anthropic.claude-code"
|
|
33
51
|
]
|
|
34
52
|
}
|
|
35
53
|
},
|
|
36
54
|
|
|
37
55
|
"workspaceFolder": "/workspaces",
|
|
38
56
|
|
|
39
|
-
"postCreateCommand": "bash
|
|
57
|
+
"postCreateCommand": "bash /workspaces/testronaut-cli/.devcontainer/setup-github-auth.sh",
|
|
40
58
|
|
|
41
|
-
"postStartCommand": "bash -
|
|
59
|
+
"postStartCommand": "bash /workspaces/testronaut-cli/.devcontainer/setup-github-auth.sh",
|
|
42
60
|
|
|
43
61
|
"hostRequirements": {
|
|
44
62
|
"cpus": 4,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
cd /workspaces
|
|
5
|
+
|
|
6
|
+
git config --global pull.rebase false
|
|
7
|
+
git config --global init.defaultBranch main
|
|
8
|
+
|
|
9
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
10
|
+
echo "GitHub CLI is not installed. Rebuild the devcontainer so the github-cli feature is installed."
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
if [ -n "${GH_TOKEN:-}" ] && ! gh auth status --hostname github.com >/dev/null 2>&1; then
|
|
15
|
+
printf '%s\n' "$GH_TOKEN" | gh auth login --hostname github.com --with-token >/dev/null
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if gh auth status --hostname github.com >/dev/null 2>&1; then
|
|
19
|
+
gh auth setup-git --hostname github.com
|
|
20
|
+
else
|
|
21
|
+
echo "GitHub CLI is not authenticated."
|
|
22
|
+
echo "Add GH_TOKEN as a Codespaces secret with access to mission-testronaut/testronaut-demo."
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
clone_if_missing() {
|
|
27
|
+
local repo="$1"
|
|
28
|
+
local directory="$2"
|
|
29
|
+
|
|
30
|
+
if [ ! -d "$directory/.git" ]; then
|
|
31
|
+
gh repo clone "$repo" "$directory"
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clone_if_missing mission-testronaut/testronaut-examples testronaut-examples
|
|
36
|
+
clone_if_missing mission-testronaut/testronaut-demo testronaut-demo
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
name: Prepare Package Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
release_type:
|
|
7
|
+
description: Semver increment to prepare
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
default: patch
|
|
11
|
+
options:
|
|
12
|
+
- patch
|
|
13
|
+
- minor
|
|
14
|
+
- major
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: prepare-package-release
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
authorize-release:
|
|
25
|
+
name: Authorize Release
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- name: Require main branch
|
|
29
|
+
run: |
|
|
30
|
+
if [ "${GITHUB_REF_NAME}" != "main" ]; then
|
|
31
|
+
echo "Manual release preparation is only allowed from main."
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
- name: Require repository admin permission
|
|
36
|
+
uses: actions/github-script@v7
|
|
37
|
+
with:
|
|
38
|
+
script: |
|
|
39
|
+
const owner = context.repo.owner;
|
|
40
|
+
const repo = context.repo.repo;
|
|
41
|
+
const actor = context.actor;
|
|
42
|
+
|
|
43
|
+
if (owner.toLowerCase() === actor.toLowerCase()) {
|
|
44
|
+
core.info(`Actor ${actor} matches the repository owner and is treated as admin.`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const response = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
49
|
+
owner,
|
|
50
|
+
repo,
|
|
51
|
+
username: actor,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { permission, role_name: roleName } = response.data;
|
|
55
|
+
core.info(`Actor ${actor} permission=${permission}, role_name=${roleName}`);
|
|
56
|
+
|
|
57
|
+
if (permission !== "admin" && roleName !== "admin") {
|
|
58
|
+
core.setFailed(`Only repository admins can prepare package releases. ${actor} has ${roleName || permission}.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
prepare-release:
|
|
62
|
+
name: Prepare Release Pull Request
|
|
63
|
+
needs: authorize-release
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
permissions:
|
|
66
|
+
# Required because this workflow pushes a release branch and opens a PR.
|
|
67
|
+
contents: write
|
|
68
|
+
pull-requests: write
|
|
69
|
+
steps:
|
|
70
|
+
- name: Checkout
|
|
71
|
+
uses: actions/checkout@v4
|
|
72
|
+
with:
|
|
73
|
+
fetch-depth: 0
|
|
74
|
+
|
|
75
|
+
- name: Setup Node
|
|
76
|
+
uses: actions/setup-node@v4
|
|
77
|
+
with:
|
|
78
|
+
node-version: 24
|
|
79
|
+
cache: npm
|
|
80
|
+
|
|
81
|
+
- name: Install dependencies
|
|
82
|
+
run: npm ci
|
|
83
|
+
|
|
84
|
+
- name: Run automated test suite
|
|
85
|
+
run: npm test
|
|
86
|
+
|
|
87
|
+
- name: Bump package version
|
|
88
|
+
run: npm version "${{ inputs.release_type }}" --no-git-tag-version
|
|
89
|
+
|
|
90
|
+
- name: Read new version
|
|
91
|
+
id: version
|
|
92
|
+
run: |
|
|
93
|
+
VERSION="$(node -e 'process.stdout.write(require("./package.json").version)')"
|
|
94
|
+
echo "value=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
95
|
+
echo "branch=release/v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
96
|
+
|
|
97
|
+
- name: Ensure release branch is not already open
|
|
98
|
+
env:
|
|
99
|
+
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
|
|
100
|
+
run: |
|
|
101
|
+
if git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then
|
|
102
|
+
echo "Release branch ${RELEASE_BRANCH} already exists on origin."
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
- name: Create release branch
|
|
107
|
+
env:
|
|
108
|
+
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
|
|
109
|
+
run: git switch -c "${RELEASE_BRANCH}"
|
|
110
|
+
|
|
111
|
+
- name: Configure git author
|
|
112
|
+
run: |
|
|
113
|
+
git config user.name "github-actions[bot]"
|
|
114
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
115
|
+
|
|
116
|
+
- name: Commit release version
|
|
117
|
+
env:
|
|
118
|
+
VERSION: ${{ steps.version.outputs.value }}
|
|
119
|
+
run: |
|
|
120
|
+
git add package.json package-lock.json
|
|
121
|
+
git commit -m "chore(release): v${VERSION}"
|
|
122
|
+
|
|
123
|
+
- name: Push release branch
|
|
124
|
+
env:
|
|
125
|
+
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
|
|
126
|
+
run: git push --set-upstream origin "${RELEASE_BRANCH}"
|
|
127
|
+
|
|
128
|
+
- name: Open release pull request
|
|
129
|
+
id: pull-request
|
|
130
|
+
uses: actions/github-script@v7
|
|
131
|
+
env:
|
|
132
|
+
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
|
|
133
|
+
VERSION: ${{ steps.version.outputs.value }}
|
|
134
|
+
with:
|
|
135
|
+
script: |
|
|
136
|
+
const owner = context.repo.owner;
|
|
137
|
+
const repo = context.repo.repo;
|
|
138
|
+
const head = process.env.RELEASE_BRANCH;
|
|
139
|
+
const version = process.env.VERSION;
|
|
140
|
+
|
|
141
|
+
const pr = await github.rest.pulls.create({
|
|
142
|
+
owner,
|
|
143
|
+
repo,
|
|
144
|
+
title: `chore(release): v${version}`,
|
|
145
|
+
head,
|
|
146
|
+
base: "main",
|
|
147
|
+
body: [
|
|
148
|
+
`Prepares \`v${version}\` for npm publication.`,
|
|
149
|
+
"",
|
|
150
|
+
"After this pull request merges into `main`, the publish workflow will release the package to npm.",
|
|
151
|
+
].join("\n"),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
core.setOutput("url", pr.data.html_url);
|
|
155
|
+
|
|
156
|
+
- name: Write job summary
|
|
157
|
+
env:
|
|
158
|
+
VERSION: ${{ steps.version.outputs.value }}
|
|
159
|
+
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
|
|
160
|
+
PR_URL: ${{ steps.pull-request.outputs.url }}
|
|
161
|
+
run: |
|
|
162
|
+
{
|
|
163
|
+
echo "## Release PR created"
|
|
164
|
+
echo
|
|
165
|
+
echo "- Version: \`${VERSION}\`"
|
|
166
|
+
echo "- Release branch: \`${RELEASE_BRANCH}\`"
|
|
167
|
+
echo "- Pull request: ${PR_URL}"
|
|
168
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- package.json
|
|
9
|
+
- package-lock.json
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: publish-package
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
publish-package:
|
|
22
|
+
name: Publish Package
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment:
|
|
25
|
+
# Use the same environment name in npm trusted publisher settings if you
|
|
26
|
+
# want npm to bind publishing to this protected environment.
|
|
27
|
+
name: package-publish
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 0
|
|
33
|
+
|
|
34
|
+
- name: Detect release version
|
|
35
|
+
id: release
|
|
36
|
+
env:
|
|
37
|
+
BEFORE_SHA: ${{ github.event.before }}
|
|
38
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
39
|
+
REF_NAME: ${{ github.ref_name }}
|
|
40
|
+
run: |
|
|
41
|
+
VERSION="$(node -e 'process.stdout.write(require("./package.json").version)')"
|
|
42
|
+
PACKAGE_NAME="$(node -e 'process.stdout.write(require("./package.json").name)')"
|
|
43
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
44
|
+
echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
|
|
45
|
+
|
|
46
|
+
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
|
47
|
+
if [ "${REF_NAME}" != "main" ]; then
|
|
48
|
+
echo "Manual publishing is only allowed from main."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ -z "${BEFORE_SHA}" ] || [ "${BEFORE_SHA}" = "0000000000000000000000000000000000000000" ]; then
|
|
57
|
+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
PREVIOUS_VERSION="$(git show "${BEFORE_SHA}:package.json" | node -e 'let data=""; process.stdin.on("data", chunk => data += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(data).version));')"
|
|
62
|
+
|
|
63
|
+
if [ "${PREVIOUS_VERSION}" = "${VERSION}" ]; then
|
|
64
|
+
echo "Version is unchanged at ${VERSION}; skipping publish."
|
|
65
|
+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
66
|
+
else
|
|
67
|
+
echo "Version changed from ${PREVIOUS_VERSION} to ${VERSION}."
|
|
68
|
+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
- name: Setup Node
|
|
72
|
+
if: steps.release.outputs.should_publish == 'true'
|
|
73
|
+
uses: actions/setup-node@v4
|
|
74
|
+
with:
|
|
75
|
+
node-version: 24
|
|
76
|
+
cache: npm
|
|
77
|
+
registry-url: https://registry.npmjs.org
|
|
78
|
+
|
|
79
|
+
- name: Install dependencies
|
|
80
|
+
if: steps.release.outputs.should_publish == 'true'
|
|
81
|
+
run: npm ci
|
|
82
|
+
|
|
83
|
+
- name: Run automated test suite
|
|
84
|
+
if: steps.release.outputs.should_publish == 'true'
|
|
85
|
+
run: npm test
|
|
86
|
+
|
|
87
|
+
- name: Check whether version already exists on npm
|
|
88
|
+
if: steps.release.outputs.should_publish == 'true'
|
|
89
|
+
id: npm-check
|
|
90
|
+
env:
|
|
91
|
+
PACKAGE_NAME: ${{ steps.release.outputs.package_name }}
|
|
92
|
+
VERSION: ${{ steps.release.outputs.version }}
|
|
93
|
+
run: |
|
|
94
|
+
if npm view "${PACKAGE_NAME}@${VERSION}" version >/dev/null 2>&1; then
|
|
95
|
+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
96
|
+
echo "Version ${VERSION} is already published to npm."
|
|
97
|
+
else
|
|
98
|
+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
99
|
+
echo "Version ${VERSION} is not yet published to npm."
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
- name: Publish to npm with trusted publishing
|
|
103
|
+
if: steps.release.outputs.should_publish == 'true' && steps.npm-check.outputs.should_publish == 'true'
|
|
104
|
+
run: npm publish --provenance --access public
|
|
105
|
+
|
|
106
|
+
- name: Tag published version
|
|
107
|
+
if: steps.release.outputs.should_publish == 'true' && steps.npm-check.outputs.should_publish == 'true'
|
|
108
|
+
env:
|
|
109
|
+
VERSION: ${{ steps.release.outputs.version }}
|
|
110
|
+
run: |
|
|
111
|
+
if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
|
|
112
|
+
echo "Tag v${VERSION} already exists locally."
|
|
113
|
+
exit 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
git config user.name "github-actions[bot]"
|
|
117
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
118
|
+
git tag "v${VERSION}"
|
|
119
|
+
git push origin "refs/tags/v${VERSION}"
|
|
120
|
+
|
|
121
|
+
- name: Write job summary
|
|
122
|
+
env:
|
|
123
|
+
VERSION: ${{ steps.release.outputs.version }}
|
|
124
|
+
SHOULD_PUBLISH: ${{ steps.release.outputs.should_publish }}
|
|
125
|
+
NPM_PUBLISH: ${{ steps.npm-check.outputs.should_publish }}
|
|
126
|
+
run: |
|
|
127
|
+
{
|
|
128
|
+
echo "## Publish summary"
|
|
129
|
+
echo
|
|
130
|
+
echo "- Version: \`${VERSION}\`"
|
|
131
|
+
echo "- Trigger requested publish: \`${SHOULD_PUBLISH:-false}\`"
|
|
132
|
+
echo "- npm publish executed: \`${NPM_PUBLISH:-false}\`"
|
|
133
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
package/bin/cli.js
CHANGED
|
@@ -364,6 +364,70 @@ if (retryFlagIndex >= 0) {
|
|
|
364
364
|
args.splice(retryFlagIndex, retryOverride ? 2 : 1);
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
// Look for --human-input / --no-human-input / --disable-human-input
|
|
368
|
+
let humanInputOverride;
|
|
369
|
+
let humanInputConsumesNext = false;
|
|
370
|
+
const humanInputFlagIndex = args.findIndex(a =>
|
|
371
|
+
a === '--human-input' ||
|
|
372
|
+
a.startsWith('--human-input=') ||
|
|
373
|
+
a === '--human_input' ||
|
|
374
|
+
a.startsWith('--human_input=') ||
|
|
375
|
+
a === '--no-human-input' ||
|
|
376
|
+
a === '--disable-human-input'
|
|
377
|
+
);
|
|
378
|
+
if (humanInputFlagIndex >= 0) {
|
|
379
|
+
const rawArg = args[humanInputFlagIndex];
|
|
380
|
+
if (rawArg.includes('=')) {
|
|
381
|
+
humanInputOverride = parseBool(rawArg.split('=')[1]);
|
|
382
|
+
} else if (rawArg === '--no-human-input' || rawArg === '--disable-human-input') {
|
|
383
|
+
humanInputOverride = false;
|
|
384
|
+
} else if (args[humanInputFlagIndex + 1] && !args[humanInputFlagIndex + 1].startsWith('-')) {
|
|
385
|
+
humanInputOverride = parseBool(args[humanInputFlagIndex + 1]);
|
|
386
|
+
humanInputConsumesNext = true;
|
|
387
|
+
} else {
|
|
388
|
+
humanInputOverride = true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (humanInputOverride !== null) {
|
|
392
|
+
process.env.TESTRONAUT_HUMAN_INPUT = humanInputOverride ? '1' : '0';
|
|
393
|
+
console.log(`👤 Human input tool ${humanInputOverride ? 'enabled' : 'disabled'} (TESTRONAUT_HUMAN_INPUT=${process.env.TESTRONAUT_HUMAN_INPUT})`);
|
|
394
|
+
} else {
|
|
395
|
+
console.warn('⚠️ Invalid --human-input value. Use true/false, 1/0, yes/no.');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const consume = 1 + (humanInputConsumesNext && humanInputOverride !== null ? 1 : 0);
|
|
399
|
+
args.splice(humanInputFlagIndex, consume);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Look for --human-input-timeout / --human_input_timeout in seconds
|
|
403
|
+
let humanInputTimeoutOverride;
|
|
404
|
+
const humanInputTimeoutFlagIndex = args.findIndex(a =>
|
|
405
|
+
a === '--human-input-timeout' ||
|
|
406
|
+
a.startsWith('--human-input-timeout=') ||
|
|
407
|
+
a === '--human_input_timeout' ||
|
|
408
|
+
a.startsWith('--human_input_timeout=')
|
|
409
|
+
);
|
|
410
|
+
if (humanInputTimeoutFlagIndex >= 0) {
|
|
411
|
+
const rawArg = args[humanInputTimeoutFlagIndex];
|
|
412
|
+
if (rawArg.includes('=')) {
|
|
413
|
+
humanInputTimeoutOverride = rawArg.split('=')[1];
|
|
414
|
+
} else if (args[humanInputTimeoutFlagIndex + 1]) {
|
|
415
|
+
humanInputTimeoutOverride = args[humanInputTimeoutFlagIndex + 1];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (humanInputTimeoutOverride) {
|
|
419
|
+
const n = Number(humanInputTimeoutOverride.trim());
|
|
420
|
+
if (Number.isFinite(n) && n > 0) {
|
|
421
|
+
process.env.TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS = String(n);
|
|
422
|
+
console.log(`⏱️ Human input timeout override: ${process.env.TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS}s`);
|
|
423
|
+
} else {
|
|
424
|
+
console.warn(`⚠️ Invalid --human-input-timeout value "${humanInputTimeoutOverride}". Ignoring.`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
args.splice(humanInputTimeoutFlagIndex, humanInputTimeoutOverride ? 2 : 1);
|
|
429
|
+
}
|
|
430
|
+
|
|
367
431
|
const allResults = [];
|
|
368
432
|
const runId = `run_${Date.now()}`;
|
|
369
433
|
const startTime = new Date();
|
|
@@ -386,6 +450,9 @@ Options:
|
|
|
386
450
|
--provider=<id> Override LLM provider for this run (e.g., --provider=openai)
|
|
387
451
|
--dev Use the staging API base URL
|
|
388
452
|
--vercel-bypass=<secret> Send Vercel protection bypass header for protected deployments
|
|
453
|
+
--human-input[=<bool>] Allow the agent to pause for short verification codes (default: true)
|
|
454
|
+
--no-human-input Disable human-in-the-loop verification prompts for automated runs
|
|
455
|
+
--human-input-timeout=<s> Override human input wait timeout in seconds (default: 60)
|
|
389
456
|
--help Show this help message
|
|
390
457
|
--retry_limit=<n> Override agent turn retry limits (minimum 1, maximum 10)
|
|
391
458
|
|
package/bin/init.js
CHANGED
|
@@ -141,6 +141,7 @@ export async function initializeTestronautProject() {
|
|
|
141
141
|
config.maxTurns = config.maxTurns ?? defaults.maxTurns;
|
|
142
142
|
config.dom = { ...defaults.dom, ...(config.dom || {}) };
|
|
143
143
|
config.resourceGuard = { ...defaults.resourceGuard, ...(config.resourceGuard || {}) };
|
|
144
|
+
config.humanInput = { ...defaults.humanInput, ...(config.humanInput || {}) };
|
|
144
145
|
|
|
145
146
|
// ─────────────────────────────────────────────
|
|
146
147
|
// STEP 4: Persist config and scaffold .env (first run only)
|
package/bin/initHelpers.js
CHANGED
package/core/agent.js
CHANGED
|
@@ -36,7 +36,7 @@ import path from 'path';
|
|
|
36
36
|
* @param {string} missionName
|
|
37
37
|
* @param {number} [maxTurns=20] - upper bound for turnLoop per goal
|
|
38
38
|
* @param {number} [retryLimit]
|
|
39
|
-
* @param {{ domListLimit?: number|typeof Infinity, debug?: boolean, resourceGuard?: { enabled:boolean, hrefIncludes:string[], dataTypes:string[] } }} [opts]
|
|
39
|
+
* @param {{ domListLimit?: number|typeof Infinity, debug?: boolean, resourceGuard?: { enabled:boolean, hrefIncludes:string[], dataTypes:string[] }, humanInput?: { enabled:boolean, timeoutSeconds:number } }} [opts]
|
|
40
40
|
* @returns {Promise<Array<{missionName:string, submissionType:string, submissionName:string|null, status:'passed'|'failed', steps:any[], startTime:number, endTime:number}>>}
|
|
41
41
|
*/
|
|
42
42
|
export async function runAgent(goals, missionName, maxTurns = 20, retryLimit, opts = {}) {
|
|
@@ -135,6 +135,17 @@ Use Ground Control as your persistent mission memory about:
|
|
|
135
135
|
- Any constraints about staying on the correct site.
|
|
136
136
|
`.trim();
|
|
137
137
|
|
|
138
|
+
if (opts.humanInput?.enabled !== false) {
|
|
139
|
+
systemContent +=
|
|
140
|
+
'\n\n' +
|
|
141
|
+
`
|
|
142
|
+
Human-in-the-loop verification:
|
|
143
|
+
- If the app requires a TOTP, SMS, email, or similar short verification code that you cannot retrieve yourself, call request_human_input.
|
|
144
|
+
- Do not ask for passwords, API keys, or long free-form text with this tool.
|
|
145
|
+
- After receiving the code from the tool result, enter it into the appropriate field and continue the mission.
|
|
146
|
+
`.trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
if (groundSummary) {
|
|
139
150
|
systemContent +=
|
|
140
151
|
'\n\n' +
|
|
@@ -164,6 +175,7 @@ Use Ground Control as your persistent mission memory about:
|
|
|
164
175
|
retryLimit,
|
|
165
176
|
groundControl,
|
|
166
177
|
resourceGuard: opts.resourceGuard,
|
|
178
|
+
humanInput: opts.humanInput,
|
|
167
179
|
onStep: (s) => {
|
|
168
180
|
// Append each step as a JSON line
|
|
169
181
|
try {
|
package/core/config.js
CHANGED
|
@@ -173,6 +173,71 @@ export function getResourceGuardConfig(cfg) {
|
|
|
173
173
|
return { enabled, hrefIncludes: finalHref, dataTypes: finalTypes };
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function parseBool(raw) {
|
|
177
|
+
if (raw === undefined || raw === null) return null;
|
|
178
|
+
const lower = String(raw).trim().toLowerCase();
|
|
179
|
+
if (['1', 'true', 'yes', 'on', 'allow', 'enabled'].includes(lower)) return true;
|
|
180
|
+
if (['0', 'false', 'no', 'off', 'deny', 'disabled'].includes(lower)) return false;
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolve human-in-the-loop verification input settings.
|
|
186
|
+
* Priority: CLI/env overrides → testronaut-config.json → built-in defaults.
|
|
187
|
+
*
|
|
188
|
+
* Supported config:
|
|
189
|
+
* humanInput: { enabled: true, timeoutSeconds: 60 }
|
|
190
|
+
* humanInputEnabled / allowHumanInput
|
|
191
|
+
* humanInputTimeoutSeconds / humanInputTimeout
|
|
192
|
+
*
|
|
193
|
+
* Supported env (set by CLI flags too):
|
|
194
|
+
* TESTRONAUT_HUMAN_INPUT=true|false
|
|
195
|
+
* TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS=60
|
|
196
|
+
*
|
|
197
|
+
* @param {object} cfg
|
|
198
|
+
* @param {{ enabled?: boolean, timeoutSeconds?: number }} [fallback]
|
|
199
|
+
* @returns {{ enabled:boolean, timeoutSeconds:number, source:{ enabled:'env'|'config'|'default', timeout:'env'|'config'|'default' }, clamped:boolean }}
|
|
200
|
+
*/
|
|
201
|
+
export function getHumanInputConfig(cfg, fallback = { enabled: true, timeoutSeconds: 60 }) {
|
|
202
|
+
const clampTimeout = (raw, fb) => {
|
|
203
|
+
const n = Number(raw);
|
|
204
|
+
if (!Number.isFinite(n)) return { value: fb, clamped: false };
|
|
205
|
+
const clamped = Math.min(300, Math.max(5, Math.trunc(n)));
|
|
206
|
+
return { value: clamped, clamped: clamped !== n };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const envEnabled = parseBool(process.env.TESTRONAUT_HUMAN_INPUT);
|
|
210
|
+
const cfgEnabled = parseBool(cfg?.humanInput?.enabled ?? cfg?.humanInputEnabled ?? cfg?.allowHumanInput);
|
|
211
|
+
const enabled =
|
|
212
|
+
envEnabled !== null ? envEnabled :
|
|
213
|
+
cfgEnabled !== null ? cfgEnabled :
|
|
214
|
+
fallback.enabled !== false;
|
|
215
|
+
const enabledSource =
|
|
216
|
+
envEnabled !== null ? 'env' :
|
|
217
|
+
cfgEnabled !== null ? 'config' :
|
|
218
|
+
'default';
|
|
219
|
+
|
|
220
|
+
const cfgTimeoutRaw = cfg?.humanInput?.timeoutSeconds ?? cfg?.humanInput?.timeout ?? cfg?.humanInputTimeoutSeconds ?? cfg?.humanInputTimeout;
|
|
221
|
+
const envTimeoutRaw = process.env.TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS;
|
|
222
|
+
let timeoutSource = 'default';
|
|
223
|
+
let timeoutRaw = fallback.timeoutSeconds ?? 60;
|
|
224
|
+
if (envTimeoutRaw !== undefined) {
|
|
225
|
+
timeoutRaw = envTimeoutRaw;
|
|
226
|
+
timeoutSource = 'env';
|
|
227
|
+
} else if (cfgTimeoutRaw !== undefined) {
|
|
228
|
+
timeoutRaw = cfgTimeoutRaw;
|
|
229
|
+
timeoutSource = 'config';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const timeout = clampTimeout(timeoutRaw, 60);
|
|
233
|
+
return {
|
|
234
|
+
enabled,
|
|
235
|
+
timeoutSeconds: timeout.value,
|
|
236
|
+
source: { enabled: enabledSource, timeout: timeoutSource },
|
|
237
|
+
clamped: timeout.clamped,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
176
241
|
/**
|
|
177
242
|
* Baseline limits for mission runs. Adjust here for global defaults.
|
|
178
243
|
*
|
package/core/redaction.js
CHANGED
|
@@ -44,8 +44,13 @@ export const SENSITIVE_KEYWORDS = [
|
|
|
44
44
|
'auth',
|
|
45
45
|
'pin',
|
|
46
46
|
'otp',
|
|
47
|
+
'code',
|
|
48
|
+
'totp',
|
|
49
|
+
'mfa',
|
|
47
50
|
];
|
|
48
51
|
|
|
52
|
+
const VERIFICATION_CODE_KEYWORDS = new Set(['otp', 'code', 'totp', 'mfa']);
|
|
53
|
+
|
|
49
54
|
/**
|
|
50
55
|
* Builds a single RegExp that treats hyphens as `[-_\s]?` to catch variants.
|
|
51
56
|
* @returns {RegExp}
|
|
@@ -141,6 +146,13 @@ function isSensitiveCall(fnName, args = {}) {
|
|
|
141
146
|
export function redactArgs(fnName, args = {}, { showLength = true } = {}) {
|
|
142
147
|
const out = clone(args);
|
|
143
148
|
|
|
149
|
+
if (fnName === 'request_human_input') {
|
|
150
|
+
for (const key of ['value', 'code', 'input', 'answer', 'redactedValue']) {
|
|
151
|
+
if (key in out) out[key] = maskPreview(out[key], showLength);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
// Mask direct props with sensitive-looking keys
|
|
145
157
|
for (const k of Object.keys(out || {})) {
|
|
146
158
|
if (hasSensitiveHint(k)) {
|
|
@@ -165,8 +177,8 @@ export function redactArgs(fnName, args = {}, { showLength = true } = {}) {
|
|
|
165
177
|
* 1) Sensitive-word ... "VALUE"
|
|
166
178
|
* e.g., `password is "hunter2"`
|
|
167
179
|
*
|
|
168
|
-
* 2) Sensitive-word ... (with|as|to
|
|
169
|
-
* e.g., `token=abcd1234`, `auth with xyz`
|
|
180
|
+
* 2) Sensitive-word ... (with|as|to|=|entered|provided|used|submitted|filled) VALUE
|
|
181
|
+
* e.g., `token=abcd1234`, `auth with xyz`, `MFA (entered 123456)`
|
|
170
182
|
*
|
|
171
183
|
* 3) "VALUE" ... into/in/on/to TARGET (TARGET contains a sensitive hint)
|
|
172
184
|
* e.g., `Type "Imp0st3r123!" into #access`
|
|
@@ -179,18 +191,29 @@ export function redactPasswordInText(text = '', { showLength = true } = {}) {
|
|
|
179
191
|
let s = String(text ?? '');
|
|
180
192
|
|
|
181
193
|
// Build a reusable source pattern for SENSITIVE_KEYWORDS with hyphen expansion
|
|
182
|
-
const
|
|
183
|
-
|
|
194
|
+
const strongKwParts = SENSITIVE_KEYWORDS
|
|
195
|
+
.filter(k => !VERIFICATION_CODE_KEYWORDS.has(k))
|
|
196
|
+
.map(k => k.replace(/-/g, '[-_\\s]?'));
|
|
197
|
+
const verificationKwParts = [...VERIFICATION_CODE_KEYWORDS].map(k => k.replace(/-/g, '[-_\\s]?'));
|
|
198
|
+
const strongKwGroup = `(${strongKwParts.join('|')})`;
|
|
199
|
+
const verificationKwGroup = `(${verificationKwParts.join('|')})`;
|
|
184
200
|
|
|
185
201
|
// Case 1: keyword ... "VALUE"
|
|
186
202
|
s = s.replace(
|
|
187
|
-
new RegExp(`(\\b${
|
|
203
|
+
new RegExp(`(\\b${strongKwGroup}\\b[^"'\\\\\\n]{0,80}(?:[:=]|\\s)["'])([^"'\\n]+)(["'])`, 'gi'),
|
|
188
204
|
(_m, pre, _kw, secret, post) => pre + maskPreview(secret, showLength) + post
|
|
189
205
|
);
|
|
190
206
|
|
|
191
|
-
// Case 2: keyword ... (with|as|to
|
|
207
|
+
// Case 2: keyword ... (with|as|to|=|entered|provided|used|submitted|filled) VALUE
|
|
208
|
+
s = s.replace(
|
|
209
|
+
new RegExp(`(\\b${strongKwGroup}\\b[^.\\n]{0,80}?(?:\\b(with|as|to|entered|provided|used|submitted|filled)\\b|=)\\s*\\(?)([^\\s"'\\\`),.;:]+)`, 'gi'),
|
|
210
|
+
(_m, pre, _kw1, _kw2, secret) => pre + maskPreview(secret, showLength)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Verification-code words are common UI text. Only redact them when the
|
|
214
|
+
// phrasing says a concrete value was entered/provided/etc.
|
|
192
215
|
s = s.replace(
|
|
193
|
-
new RegExp(`(\\b${
|
|
216
|
+
new RegExp(`(\\b${verificationKwGroup}\\b[^.\\n]{0,80}?\\b(entered|provided|used|submitted|filled)\\b\\s*\\(?)([A-Za-z0-9_-]{4,64})`, 'gi'),
|
|
194
217
|
(_m, pre, _kw1, _kw2, secret) => pre + maskPreview(secret, showLength)
|
|
195
218
|
);
|
|
196
219
|
|