testronaut 1.3.2 โ†’ 1.3.4

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,48 @@
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
+ ]
34
+ }
35
+ },
36
+
37
+ "workspaceFolder": "/workspaces",
38
+
39
+ "postCreateCommand": "bash -lc 'set -e; cd /workspaces; if [ ! -d \"testronaut-examples/.git\" ]; then git clone https://github.com/mission-testronaut/testronaut-examples.git; fi'",
40
+
41
+ "postStartCommand": "bash -lc 'git config --global pull.rebase false && git config --global init.defaultBranch main'",
42
+
43
+ "hostRequirements": {
44
+ "cpus": 4,
45
+ "memory": "8gb",
46
+ "storage": "32gb"
47
+ }
48
+ }
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
@@ -11,8 +11,10 @@
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
17
+ * --dev โ†’ use staging API base URL
16
18
  * --help โ†’ prints help
17
19
  *
18
20
  * Notable helpers (defined below):
@@ -42,6 +44,7 @@ import inquirer from 'inquirer';
42
44
  import fetch from 'node-fetch';
43
45
  import crypto from 'crypto';
44
46
  import http from 'http';
47
+ import { resolveProviderModel } from '../llm/modelResolver.js';
45
48
  import { exec as execCmd } from 'child_process';
46
49
  import { promisify } from 'util';
47
50
  const exec = promisify(execCmd);
@@ -55,11 +58,106 @@ process.env.PLAYWRIGHT_BROWSERS_PATH = process.env.PLAYWRIGHT_BROWSERS_PATH || '
55
58
  const TMP_DIR = path.resolve('./missions/tmp');
56
59
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
57
60
 
58
- // const apiBase = `http://localhost:3002` //
59
- const apiBase = 'http://api.testronaut.app'; // Replace with your actual API base URL
61
+ const DEFAULT_API_BASE = 'http://api.testronaut.app';
62
+ const DEV_API_BASE = 'https://staging.api.testronaut.app';
60
63
 
64
+ let args = process.argv.slice(2);
61
65
 
62
- const args = process.argv.slice(2);
66
+ // Look for --dev / --developer / --developer-mode / --staging
67
+ const devFlagIndex = args.findIndex(a =>
68
+ a === '--dev' ||
69
+ a === '--developer' ||
70
+ a === '--developer-mode' ||
71
+ a === '--staging'
72
+ );
73
+ let apiBase = process.env.TESTRONAUT_API_BASE || DEFAULT_API_BASE;
74
+ if (devFlagIndex >= 0) {
75
+ apiBase = DEV_API_BASE;
76
+ console.log(`๐Ÿงช Developer mode: using ${apiBase}`);
77
+ args.splice(devFlagIndex, 1);
78
+ }
79
+
80
+ function parseVercelBypassArgs(argsList) {
81
+ const nextArgs = [...argsList];
82
+ let secret;
83
+ let invalid = false;
84
+ const idx = nextArgs.findIndex(a =>
85
+ a === '--vercel-bypass' ||
86
+ a.startsWith('--vercel-bypass=') ||
87
+ a === '--vercel_bypass' ||
88
+ a.startsWith('--vercel_bypass=')
89
+ );
90
+ if (idx >= 0) {
91
+ const rawArg = nextArgs[idx];
92
+ const hasInline = rawArg.includes('=');
93
+ if (hasInline) {
94
+ secret = rawArg.split('=')[1];
95
+ } else if (nextArgs[idx + 1]) {
96
+ secret = nextArgs[idx + 1];
97
+ }
98
+
99
+ if (!secret) invalid = true;
100
+
101
+ // Remove flag & value so they arenโ€™t treated as filenames
102
+ const consume = hasInline ? 1 : (secret ? 2 : 1);
103
+ nextArgs.splice(idx, consume);
104
+ }
105
+ return { secret, args: nextArgs, invalid };
106
+ }
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
+
142
+ const vercelBypassResult = parseVercelBypassArgs(args);
143
+ if (vercelBypassResult.invalid) {
144
+ console.warn('โš ๏ธ Invalid --vercel-bypass value. Provide a non-empty secret.');
145
+ }
146
+ args = vercelBypassResult.args;
147
+ const vercelBypassOverride = vercelBypassResult.secret;
148
+
149
+ const vercelBypassSecret =
150
+ vercelBypassOverride ||
151
+ process.env.VERCEL_AUTOMATION_BYPASS_SECRET ||
152
+ process.env.TESTRONAUT_VERCEL_BYPASS;
153
+ function createVercelBypassHeader(secret) {
154
+ return secret ? { 'x-vercel-protection-bypass': secret } : {};
155
+ }
156
+ const vercelBypassHeader = createVercelBypassHeader(vercelBypassSecret);
157
+
158
+ function buildApiHeaders(extra = {}) {
159
+ return { ...extra, ...vercelBypassHeader };
160
+ }
63
161
 
64
162
  /**
65
163
  * Read a JSONL steps file into an array of objects.
@@ -133,6 +231,9 @@ export const __test__ = {
133
231
  findLatestReportPair,
134
232
  mergeDuplicateTurns,
135
233
  readJsonlSteps,
234
+ parseVercelBypassArgs,
235
+ createVercelBypassHeader,
236
+ parseProviderArgs,
136
237
  };
137
238
 
138
239
  // Look for --model=<id> or --model <id>
@@ -154,6 +255,18 @@ if (modelFlagIndex >= 0) {
154
255
  args.splice(modelFlagIndex, modelOverride ? 2 : 1);
155
256
  }
156
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
+
157
270
  // Look for --debug / --debug=<bool> / --no-debug
158
271
  // Normalize CLI boolean strings into true/false/null for optional flags.
159
272
  const parseBool = (v) => {
@@ -270,6 +383,9 @@ Options:
270
383
  --init Scaffold project folders and a welcome mission
271
384
  --turns=<n> Override max turns for this run (e.g., --turns=30)
272
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)
387
+ --dev Use the staging API base URL
388
+ --vercel-bypass=<secret> Send Vercel protection bypass header for protected deployments
273
389
  --help Show this help message
274
390
  --retry_limit=<n> Override agent turn retry limits (minimum 1, maximum 10)
275
391
 
@@ -403,25 +519,8 @@ const flatMissions = allResults.flatMap(entry => {
403
519
  });
404
520
  });
405
521
 
406
- // Read provider/model from config (allow env override for model)
407
- const cfgPath = path.resolve(process.cwd(), 'testronaut-config.json');
408
- let llmProvider = 'openai';
409
- let llmModel = 'gpt-4o';
410
-
411
- if (fs.existsSync(cfgPath)) {
412
- try {
413
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
414
- if (cfg?.provider) llmProvider = String(cfg.provider);
415
- if (cfg?.model) llmModel = String(cfg.model);
416
- } catch (e) {
417
- console.warn('โš ๏ธ Could not read testronaut-config.json:', e.message);
418
- }
419
- }
420
-
421
- // Env override takes precedence for the model (matches runtime behavior)
422
- if (process.env.TESTRONAUT_MODEL?.trim()) {
423
- llmModel = process.env.TESTRONAUT_MODEL.trim();
424
- }
522
+ // Read provider/model from config (allow env override)
523
+ const { provider: llmProvider, model: llmModel } = resolveProviderModel({ cwd: process.cwd() });
425
524
 
426
525
 
427
526
  const report = {
@@ -592,13 +691,13 @@ async function handleLogin() {
592
691
  try {
593
692
  const response = await fetch(`${apiBase}/api/user/cli`, { // Replace with your actual URL
594
693
  method: 'POST',
595
- headers: {
694
+ headers: buildApiHeaders({
596
695
  'Content-Type': 'application/x-www-form-urlencoded',
597
- },
696
+ }),
598
697
  body: formData.toString(),
599
698
  });
600
699
 
601
- const data = await response.json();
700
+ const data = await parseJsonSafe(response, 'login');
602
701
 
603
702
  if (data.error) {
604
703
  console.error('โŒ Authentication failed:', data.error);
@@ -659,7 +758,7 @@ async function uploadReport() {
659
758
  try {
660
759
  const res = await fetch(`${apiBase}/api/user/cli/${sessionToken}`, {
661
760
  method: 'POST',
662
- headers: { 'Content-Type': 'application/json' },
761
+ headers: buildApiHeaders({ 'Content-Type': 'application/json' }),
663
762
  body: reportJson,
664
763
  });
665
764
  const data = await res.json();
@@ -735,7 +834,7 @@ async function uploadReport() {
735
834
  // console.log(` the fetch url is: ${apiBase}/api/user/cli/${sessionToken}/uploads/start`);
736
835
  const startRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/start`, {
737
836
  method: 'POST',
738
- headers: { 'Content-Type': 'application/json' },
837
+ headers: buildApiHeaders({ 'Content-Type': 'application/json' }),
739
838
  body: JSON.stringify({ reportId: savedReportId, stepIndex, fileName: path.basename(filePath), mime, size: stat.size, sha1: hash }),
740
839
  });
741
840
  const start = await parseJsonSafe(startRes, 'uploads/start');
@@ -757,7 +856,7 @@ async function uploadReport() {
757
856
  // console.log(` finalizing uploadโ€ฆ`);
758
857
  const finishRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/finish`, {
759
858
  method: 'POST',
760
- headers: { 'Content-Type': 'application/json' },
859
+ headers: buildApiHeaders({ 'Content-Type': 'application/json' }),
761
860
  body: JSON.stringify({
762
861
  reportId: savedReportId,
763
862
  stepIndex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "bin/cli.js"
@@ -42,4 +42,77 @@ 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('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
+
113
+ it('builds the vercel bypass header when provided', () => {
114
+ const { createVercelBypassHeader } = __test__;
115
+ expect(createVercelBypassHeader('abc')).toEqual({ 'x-vercel-protection-bypass': 'abc' });
116
+ expect(createVercelBypassHeader('')).toEqual({});
117
+ });
45
118
  });