testronaut 1.3.3 → 1.3.5
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.
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Testronaut Multi-Repo Codespace",
|
|
3
|
+
|
|
4
|
+
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
|
|
5
|
+
|
|
6
|
+
"customizations": {
|
|
7
|
+
"codespaces": {
|
|
8
|
+
"repositories": {
|
|
9
|
+
"mission-testronaut/testronaut-cli": {
|
|
10
|
+
"permissions": "write-all"
|
|
11
|
+
},
|
|
12
|
+
"mission-testronaut/testronaut-examples": {
|
|
13
|
+
"permissions": "write-all"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"vscode": {
|
|
18
|
+
"settings": {
|
|
19
|
+
"files.autoSave": "onFocusChange",
|
|
20
|
+
"terminal.integrated.defaultProfile.linux": "bash",
|
|
21
|
+
"git.confirmSync": false,
|
|
22
|
+
"explorer.confirmDelete": false,
|
|
23
|
+
"extensions.autoCheckUpdates": true,
|
|
24
|
+
"extensions.autoUpdate": true
|
|
25
|
+
},
|
|
26
|
+
"extensions": [
|
|
27
|
+
"github.vscode-github-actions",
|
|
28
|
+
"github.codespaces",
|
|
29
|
+
"eamodio.gitlens",
|
|
30
|
+
"esbenp.prettier-vscode",
|
|
31
|
+
"dbaeumer.vscode-eslint",
|
|
32
|
+
"openai.chatgpt",
|
|
33
|
+
"anthropic.claude-code"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"workspaceFolder": "/workspaces",
|
|
39
|
+
|
|
40
|
+
"postCreateCommand": "bash -lc 'set -e; cd /workspaces; if [ ! -d \"testronaut-examples/.git\" ]; then git clone https://github.com/mission-testronaut/testronaut-examples.git; fi'",
|
|
41
|
+
|
|
42
|
+
"postStartCommand": "bash -lc 'git config --global pull.rebase false && git config --global init.defaultBranch main'",
|
|
43
|
+
|
|
44
|
+
"hostRequirements": {
|
|
45
|
+
"cpus": 4,
|
|
46
|
+
"memory": "8gb",
|
|
47
|
+
"storage": "32gb"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Key flags:
|
|
13
13
|
* --model <id> / --model=<id> → sets TESTRONAUT_MODEL env (wins over config file)
|
|
14
|
+
* --provider <id> / --provider=<id> → sets TESTRONAUT_PROVIDER env (wins over config file)
|
|
14
15
|
* --turns <n> / --turns=<n> → sets TESTRONAUT_TURNS env (wins over config file)
|
|
15
16
|
* --init → scaffolds project + optional Playwright browsers
|
|
16
17
|
* --dev → use staging API base URL
|
|
@@ -43,6 +44,7 @@ import inquirer from 'inquirer';
|
|
|
43
44
|
import fetch from 'node-fetch';
|
|
44
45
|
import crypto from 'crypto';
|
|
45
46
|
import http from 'http';
|
|
47
|
+
import { resolveProviderModel } from '../llm/modelResolver.js';
|
|
46
48
|
import { exec as execCmd } from 'child_process';
|
|
47
49
|
import { promisify } from 'util';
|
|
48
50
|
const exec = promisify(execCmd);
|
|
@@ -103,6 +105,40 @@ function parseVercelBypassArgs(argsList) {
|
|
|
103
105
|
return { secret, args: nextArgs, invalid };
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
function parseProviderArgs(argsList) {
|
|
109
|
+
const nextArgs = [...argsList];
|
|
110
|
+
let provider;
|
|
111
|
+
let invalid = false;
|
|
112
|
+
const idx = nextArgs.findIndex(a => a === '--provider' || a.startsWith('--provider='));
|
|
113
|
+
if (idx >= 0) {
|
|
114
|
+
const rawArg = nextArgs[idx];
|
|
115
|
+
const hasInline = rawArg.includes('=');
|
|
116
|
+
const nextVal = !hasInline && nextArgs[idx + 1] && !nextArgs[idx + 1].startsWith('-') ? nextArgs[idx + 1] : undefined;
|
|
117
|
+
if (hasInline) {
|
|
118
|
+
provider = rawArg.split('=')[1];
|
|
119
|
+
} else if (nextVal) {
|
|
120
|
+
provider = nextVal;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (provider) {
|
|
124
|
+
provider = provider.trim();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const supportedProviders = new Set(['openai', 'gemini']);
|
|
128
|
+
const isValid = (v) => !!v && supportedProviders.has(String(v).toLowerCase());
|
|
129
|
+
if (!isValid(provider)) {
|
|
130
|
+
invalid = true;
|
|
131
|
+
provider = undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove flag & value so they aren’t treated as filenames
|
|
135
|
+
const consume = hasInline ? 1 : (nextVal ? 2 : 1);
|
|
136
|
+
nextArgs.splice(idx, consume);
|
|
137
|
+
}
|
|
138
|
+
return { provider, args: nextArgs, invalid };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
106
142
|
const vercelBypassResult = parseVercelBypassArgs(args);
|
|
107
143
|
if (vercelBypassResult.invalid) {
|
|
108
144
|
console.warn('⚠️ Invalid --vercel-bypass value. Provide a non-empty secret.');
|
|
@@ -197,6 +233,7 @@ export const __test__ = {
|
|
|
197
233
|
readJsonlSteps,
|
|
198
234
|
parseVercelBypassArgs,
|
|
199
235
|
createVercelBypassHeader,
|
|
236
|
+
parseProviderArgs,
|
|
200
237
|
};
|
|
201
238
|
|
|
202
239
|
// Look for --model=<id> or --model <id>
|
|
@@ -218,6 +255,18 @@ if (modelFlagIndex >= 0) {
|
|
|
218
255
|
args.splice(modelFlagIndex, modelOverride ? 2 : 1);
|
|
219
256
|
}
|
|
220
257
|
|
|
258
|
+
// Look for --provider=<id> or --provider <id>
|
|
259
|
+
const providerResult = parseProviderArgs(args);
|
|
260
|
+
if (providerResult.invalid) {
|
|
261
|
+
console.warn('⚠️ Invalid --provider value. Provide a non-empty provider id.');
|
|
262
|
+
}
|
|
263
|
+
args = providerResult.args;
|
|
264
|
+
const providerOverride = providerResult.provider;
|
|
265
|
+
if (providerOverride) {
|
|
266
|
+
process.env.TESTRONAUT_PROVIDER = providerOverride.trim();
|
|
267
|
+
console.log(`🧩 Provider override: ${process.env.TESTRONAUT_PROVIDER}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
221
270
|
// Look for --debug / --debug=<bool> / --no-debug
|
|
222
271
|
// Normalize CLI boolean strings into true/false/null for optional flags.
|
|
223
272
|
const parseBool = (v) => {
|
|
@@ -334,6 +383,7 @@ Options:
|
|
|
334
383
|
--init Scaffold project folders and a welcome mission
|
|
335
384
|
--turns=<n> Override max turns for this run (e.g., --turns=30)
|
|
336
385
|
--debug[=<bool>] Enable verbose debug logs (or set TESTRONAUT_DEBUG=1)
|
|
386
|
+
--provider=<id> Override LLM provider for this run (e.g., --provider=openai)
|
|
337
387
|
--dev Use the staging API base URL
|
|
338
388
|
--vercel-bypass=<secret> Send Vercel protection bypass header for protected deployments
|
|
339
389
|
--help Show this help message
|
|
@@ -469,25 +519,8 @@ const flatMissions = allResults.flatMap(entry => {
|
|
|
469
519
|
});
|
|
470
520
|
});
|
|
471
521
|
|
|
472
|
-
// Read provider/model from config (allow env override
|
|
473
|
-
const
|
|
474
|
-
let llmProvider = 'openai';
|
|
475
|
-
let llmModel = 'gpt-4o';
|
|
476
|
-
|
|
477
|
-
if (fs.existsSync(cfgPath)) {
|
|
478
|
-
try {
|
|
479
|
-
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
480
|
-
if (cfg?.provider) llmProvider = String(cfg.provider);
|
|
481
|
-
if (cfg?.model) llmModel = String(cfg.model);
|
|
482
|
-
} catch (e) {
|
|
483
|
-
console.warn('⚠️ Could not read testronaut-config.json:', e.message);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Env override takes precedence for the model (matches runtime behavior)
|
|
488
|
-
if (process.env.TESTRONAUT_MODEL?.trim()) {
|
|
489
|
-
llmModel = process.env.TESTRONAUT_MODEL.trim();
|
|
490
|
-
}
|
|
522
|
+
// Read provider/model from config (allow env override)
|
|
523
|
+
const { provider: llmProvider, model: llmModel } = resolveProviderModel({ cwd: process.cwd() });
|
|
491
524
|
|
|
492
525
|
|
|
493
526
|
const report = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testronaut",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"main": "./runner/testronaut.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"testronaut": "bin/cli.js"
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"author": "",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"description": "",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/mission-testronaut/testronaut-cli"
|
|
25
|
+
},
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"@dqbd/tiktoken": "^1.0.21",
|
|
24
28
|
"@google/generative-ai": "^0.24.1",
|
|
@@ -67,6 +67,49 @@ describe('cli helpers', () => {
|
|
|
67
67
|
expect(res.invalid).toBe(true);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it('parses provider flag with inline value and removes it from args', () => {
|
|
71
|
+
const { parseProviderArgs } = __test__;
|
|
72
|
+
const res = parseProviderArgs(['--provider=openai', 'login']);
|
|
73
|
+
expect(res.provider).toBe('openai');
|
|
74
|
+
expect(res.args).toEqual(['login']);
|
|
75
|
+
expect(res.invalid).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('parses provider flag value from next arg', () => {
|
|
79
|
+
const { parseProviderArgs } = __test__;
|
|
80
|
+
const res = parseProviderArgs(['--provider', 'gemini', 'login']);
|
|
81
|
+
expect(res.provider).toBe('gemini');
|
|
82
|
+
expect(res.args).toEqual(['login']);
|
|
83
|
+
expect(res.invalid).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('accepts supported provider names', () => {
|
|
87
|
+
const { parseProviderArgs } = __test__;
|
|
88
|
+
const res1 = parseProviderArgs(['--provider', 'openai', 'login']);
|
|
89
|
+
expect(res1.provider).toBe('openai');
|
|
90
|
+
expect(res1.invalid).toBe(false);
|
|
91
|
+
|
|
92
|
+
const res2 = parseProviderArgs(['--provider=gemini', 'login']);
|
|
93
|
+
expect(res2.provider).toBe('gemini');
|
|
94
|
+
expect(res2.invalid).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('flags invalid provider name', () => {
|
|
98
|
+
const { parseProviderArgs } = __test__;
|
|
99
|
+
const res = parseProviderArgs(['--provider', 'bad provider', 'login']);
|
|
100
|
+
expect(res.provider).toBeUndefined();
|
|
101
|
+
expect(res.args).toEqual(['login']);
|
|
102
|
+
expect(res.invalid).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('handles missing provider value', () => {
|
|
106
|
+
const { parseProviderArgs } = __test__;
|
|
107
|
+
const res = parseProviderArgs(['--provider']);
|
|
108
|
+
expect(res.provider).toBeUndefined();
|
|
109
|
+
expect(res.args).toEqual([]);
|
|
110
|
+
expect(res.invalid).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
70
113
|
it('builds the vercel bypass header when provided', () => {
|
|
71
114
|
const { createVercelBypassHeader } = __test__;
|
|
72
115
|
expect(createVercelBypassHeader('abc')).toEqual({ 'x-vercel-protection-bypass': 'abc' });
|
|
@@ -35,8 +35,8 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
35
35
|
// Escaping helpers for text and HTML attributes
|
|
36
36
|
const esc = (s) => String(s ?? '')
|
|
37
37
|
.replace(/&/g, '&').replace(/</g, '<')
|
|
38
|
-
.replace(/>/g, '>')
|
|
39
|
-
|
|
38
|
+
.replace(/>/g, '>')
|
|
39
|
+
.replace(/"/g, '"');
|
|
40
40
|
const badge = (status) =>
|
|
41
41
|
status === 'passed' ? '✅ Passed' :
|
|
42
42
|
status === 'failed' ? '❌ Failed' : (status || '—');
|
|
@@ -70,7 +70,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
70
70
|
? step.summary.trim()
|
|
71
71
|
: '';
|
|
72
72
|
const planSpan = plan
|
|
73
|
-
? `<span class="plan" title="${
|
|
73
|
+
? `<span class="plan" title="${esc(plan)}">${esc(plan)}</span>`
|
|
74
74
|
: '';
|
|
75
75
|
|
|
76
76
|
return `
|
|
@@ -78,7 +78,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
78
78
|
<summary>
|
|
79
79
|
<span class="turn">Turn ${esc((step.turn ?? idx) + 1)}${esc(attemptLabel)}</span>
|
|
80
80
|
${planSpan}
|
|
81
|
-
<span class="step-result ${ok ? 'ok' : 'bad'}" ${resultTooltip ? `title="${
|
|
81
|
+
<span class="step-result ${ok ? 'ok' : 'bad'}" ${resultTooltip ? `title="${esc(resultTooltip)}"` : ''}>${esc(resultRaw)}</span>
|
|
82
82
|
<span class="tokens">tokens: ${esc(step.tokensUsed ?? '—')} / total: ${esc(step.totalTokensUsed ?? '—')}</span>
|
|
83
83
|
</summary>
|
|
84
84
|
<pre class="events">${esc(events.join('\n')) || '(no events)'}</pre>
|