testronaut 1.3.2 → 1.3.3
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 +21 -0
- package/bin/cli.js +75 -9
- package/package.json +1 -1
- package/tests/binTests/cli.helpers.test.js +30 -0
package/README.md
CHANGED
|
@@ -154,6 +154,27 @@ await runMissions({
|
|
|
154
154
|
postMission: logoutMission,
|
|
155
155
|
}, "Contact Form Flow");
|
|
156
156
|
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 🧰 Developer Mode (Staging API)
|
|
161
|
+
|
|
162
|
+
Use the staging API base URL:
|
|
163
|
+
```bash
|
|
164
|
+
npx testronaut --dev
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If the staging deployment is protected by Vercel, pass the bypass secret:
|
|
168
|
+
```bash
|
|
169
|
+
npx testronaut --dev --vercel-bypass=YOUR_SECRET login
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
You can also set the bypass secret via environment variables:
|
|
173
|
+
```bash
|
|
174
|
+
export VERCEL_AUTOMATION_BYPASS_SECRET=YOUR_SECRET
|
|
175
|
+
# or
|
|
176
|
+
export TESTRONAUT_VERCEL_BYPASS=YOUR_SECRET
|
|
177
|
+
```
|
|
157
178
|
---
|
|
158
179
|
|
|
159
180
|
## 📋 Reports
|
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* --model <id> / --model=<id> → sets TESTRONAUT_MODEL env (wins over config file)
|
|
14
14
|
* --turns <n> / --turns=<n> → sets TESTRONAUT_TURNS env (wins over config file)
|
|
15
15
|
* --init → scaffolds project + optional Playwright browsers
|
|
16
|
+
* --dev → use staging API base URL
|
|
16
17
|
* --help → prints help
|
|
17
18
|
*
|
|
18
19
|
* Notable helpers (defined below):
|
|
@@ -55,11 +56,72 @@ process.env.PLAYWRIGHT_BROWSERS_PATH = process.env.PLAYWRIGHT_BROWSERS_PATH || '
|
|
|
55
56
|
const TMP_DIR = path.resolve('./missions/tmp');
|
|
56
57
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
const
|
|
59
|
+
const DEFAULT_API_BASE = 'http://api.testronaut.app';
|
|
60
|
+
const DEV_API_BASE = 'https://staging.api.testronaut.app';
|
|
60
61
|
|
|
62
|
+
let args = process.argv.slice(2);
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
// Look for --dev / --developer / --developer-mode / --staging
|
|
65
|
+
const devFlagIndex = args.findIndex(a =>
|
|
66
|
+
a === '--dev' ||
|
|
67
|
+
a === '--developer' ||
|
|
68
|
+
a === '--developer-mode' ||
|
|
69
|
+
a === '--staging'
|
|
70
|
+
);
|
|
71
|
+
let apiBase = process.env.TESTRONAUT_API_BASE || DEFAULT_API_BASE;
|
|
72
|
+
if (devFlagIndex >= 0) {
|
|
73
|
+
apiBase = DEV_API_BASE;
|
|
74
|
+
console.log(`🧪 Developer mode: using ${apiBase}`);
|
|
75
|
+
args.splice(devFlagIndex, 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseVercelBypassArgs(argsList) {
|
|
79
|
+
const nextArgs = [...argsList];
|
|
80
|
+
let secret;
|
|
81
|
+
let invalid = false;
|
|
82
|
+
const idx = nextArgs.findIndex(a =>
|
|
83
|
+
a === '--vercel-bypass' ||
|
|
84
|
+
a.startsWith('--vercel-bypass=') ||
|
|
85
|
+
a === '--vercel_bypass' ||
|
|
86
|
+
a.startsWith('--vercel_bypass=')
|
|
87
|
+
);
|
|
88
|
+
if (idx >= 0) {
|
|
89
|
+
const rawArg = nextArgs[idx];
|
|
90
|
+
const hasInline = rawArg.includes('=');
|
|
91
|
+
if (hasInline) {
|
|
92
|
+
secret = rawArg.split('=')[1];
|
|
93
|
+
} else if (nextArgs[idx + 1]) {
|
|
94
|
+
secret = nextArgs[idx + 1];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!secret) invalid = true;
|
|
98
|
+
|
|
99
|
+
// Remove flag & value so they aren’t treated as filenames
|
|
100
|
+
const consume = hasInline ? 1 : (secret ? 2 : 1);
|
|
101
|
+
nextArgs.splice(idx, consume);
|
|
102
|
+
}
|
|
103
|
+
return { secret, args: nextArgs, invalid };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const vercelBypassResult = parseVercelBypassArgs(args);
|
|
107
|
+
if (vercelBypassResult.invalid) {
|
|
108
|
+
console.warn('⚠️ Invalid --vercel-bypass value. Provide a non-empty secret.');
|
|
109
|
+
}
|
|
110
|
+
args = vercelBypassResult.args;
|
|
111
|
+
const vercelBypassOverride = vercelBypassResult.secret;
|
|
112
|
+
|
|
113
|
+
const vercelBypassSecret =
|
|
114
|
+
vercelBypassOverride ||
|
|
115
|
+
process.env.VERCEL_AUTOMATION_BYPASS_SECRET ||
|
|
116
|
+
process.env.TESTRONAUT_VERCEL_BYPASS;
|
|
117
|
+
function createVercelBypassHeader(secret) {
|
|
118
|
+
return secret ? { 'x-vercel-protection-bypass': secret } : {};
|
|
119
|
+
}
|
|
120
|
+
const vercelBypassHeader = createVercelBypassHeader(vercelBypassSecret);
|
|
121
|
+
|
|
122
|
+
function buildApiHeaders(extra = {}) {
|
|
123
|
+
return { ...extra, ...vercelBypassHeader };
|
|
124
|
+
}
|
|
63
125
|
|
|
64
126
|
/**
|
|
65
127
|
* Read a JSONL steps file into an array of objects.
|
|
@@ -133,6 +195,8 @@ export const __test__ = {
|
|
|
133
195
|
findLatestReportPair,
|
|
134
196
|
mergeDuplicateTurns,
|
|
135
197
|
readJsonlSteps,
|
|
198
|
+
parseVercelBypassArgs,
|
|
199
|
+
createVercelBypassHeader,
|
|
136
200
|
};
|
|
137
201
|
|
|
138
202
|
// Look for --model=<id> or --model <id>
|
|
@@ -270,6 +334,8 @@ Options:
|
|
|
270
334
|
--init Scaffold project folders and a welcome mission
|
|
271
335
|
--turns=<n> Override max turns for this run (e.g., --turns=30)
|
|
272
336
|
--debug[=<bool>] Enable verbose debug logs (or set TESTRONAUT_DEBUG=1)
|
|
337
|
+
--dev Use the staging API base URL
|
|
338
|
+
--vercel-bypass=<secret> Send Vercel protection bypass header for protected deployments
|
|
273
339
|
--help Show this help message
|
|
274
340
|
--retry_limit=<n> Override agent turn retry limits (minimum 1, maximum 10)
|
|
275
341
|
|
|
@@ -592,13 +658,13 @@ async function handleLogin() {
|
|
|
592
658
|
try {
|
|
593
659
|
const response = await fetch(`${apiBase}/api/user/cli`, { // Replace with your actual URL
|
|
594
660
|
method: 'POST',
|
|
595
|
-
headers: {
|
|
661
|
+
headers: buildApiHeaders({
|
|
596
662
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
597
|
-
},
|
|
663
|
+
}),
|
|
598
664
|
body: formData.toString(),
|
|
599
665
|
});
|
|
600
666
|
|
|
601
|
-
const data = await response
|
|
667
|
+
const data = await parseJsonSafe(response, 'login');
|
|
602
668
|
|
|
603
669
|
if (data.error) {
|
|
604
670
|
console.error('❌ Authentication failed:', data.error);
|
|
@@ -659,7 +725,7 @@ async function uploadReport() {
|
|
|
659
725
|
try {
|
|
660
726
|
const res = await fetch(`${apiBase}/api/user/cli/${sessionToken}`, {
|
|
661
727
|
method: 'POST',
|
|
662
|
-
headers: { 'Content-Type': 'application/json' },
|
|
728
|
+
headers: buildApiHeaders({ 'Content-Type': 'application/json' }),
|
|
663
729
|
body: reportJson,
|
|
664
730
|
});
|
|
665
731
|
const data = await res.json();
|
|
@@ -735,7 +801,7 @@ async function uploadReport() {
|
|
|
735
801
|
// console.log(` the fetch url is: ${apiBase}/api/user/cli/${sessionToken}/uploads/start`);
|
|
736
802
|
const startRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/start`, {
|
|
737
803
|
method: 'POST',
|
|
738
|
-
headers: { 'Content-Type': 'application/json' },
|
|
804
|
+
headers: buildApiHeaders({ 'Content-Type': 'application/json' }),
|
|
739
805
|
body: JSON.stringify({ reportId: savedReportId, stepIndex, fileName: path.basename(filePath), mime, size: stat.size, sha1: hash }),
|
|
740
806
|
});
|
|
741
807
|
const start = await parseJsonSafe(startRes, 'uploads/start');
|
|
@@ -757,7 +823,7 @@ async function uploadReport() {
|
|
|
757
823
|
// console.log(` finalizing upload…`);
|
|
758
824
|
const finishRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/finish`, {
|
|
759
825
|
method: 'POST',
|
|
760
|
-
headers: { 'Content-Type': 'application/json' },
|
|
826
|
+
headers: buildApiHeaders({ 'Content-Type': 'application/json' }),
|
|
761
827
|
body: JSON.stringify({
|
|
762
828
|
reportId: savedReportId,
|
|
763
829
|
stepIndex,
|
package/package.json
CHANGED
|
@@ -42,4 +42,34 @@ describe('cli helpers', () => {
|
|
|
42
42
|
expect(parseBool('')).toBeNull();
|
|
43
43
|
expect(parseBool('maybe')).toBeNull();
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('parses vercel bypass flags and removes them from args', () => {
|
|
47
|
+
const { parseVercelBypassArgs } = __test__;
|
|
48
|
+
const res = parseVercelBypassArgs(['--dev', '--vercel-bypass=secret123', 'login']);
|
|
49
|
+
expect(res.secret).toBe('secret123');
|
|
50
|
+
expect(res.args).toEqual(['--dev', 'login']);
|
|
51
|
+
expect(res.invalid).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('parses vercel bypass value from next arg', () => {
|
|
55
|
+
const { parseVercelBypassArgs } = __test__;
|
|
56
|
+
const res = parseVercelBypassArgs(['--vercel_bypass', 'abc', 'login']);
|
|
57
|
+
expect(res.secret).toBe('abc');
|
|
58
|
+
expect(res.args).toEqual(['login']);
|
|
59
|
+
expect(res.invalid).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('handles missing vercel bypass value', () => {
|
|
63
|
+
const { parseVercelBypassArgs } = __test__;
|
|
64
|
+
const res = parseVercelBypassArgs(['--vercel-bypass']);
|
|
65
|
+
expect(res.secret).toBeUndefined();
|
|
66
|
+
expect(res.args).toEqual([]);
|
|
67
|
+
expect(res.invalid).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('builds the vercel bypass header when provided', () => {
|
|
71
|
+
const { createVercelBypassHeader } = __test__;
|
|
72
|
+
expect(createVercelBypassHeader('abc')).toEqual({ 'x-vercel-protection-bypass': 'abc' });
|
|
73
|
+
expect(createVercelBypassHeader('')).toEqual({});
|
|
74
|
+
});
|
|
45
75
|
});
|