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 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
- // const apiBase = `http://localhost:3002` //
59
- const apiBase = 'http://api.testronaut.app'; // Replace with your actual API base URL
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
- const args = process.argv.slice(2);
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.json();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "bin/cli.js"
@@ -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
  });