testronaut 1.0.15 → 1.0.17

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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/bin/cli.js +160 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,6 +24,22 @@ Join our Discord to connect with other Testronauts, get support, and help shape
24
24
 
25
25
  ---
26
26
 
27
+ ## 📖 Documentation
28
+
29
+ Looking for deeper guides, API references, and examples?
30
+ Head over to the official Testronaut Docs:
31
+
32
+ [![Read the Docs](https://img.shields.io/badge/Read%20the%20Docs-2E8555?style=for-the-badge&logo=readthedocs&logoColor=white)](https://docs.testronaut.app)
33
+
34
+ The docs cover:
35
+ - Getting started step-by-step
36
+ - Writing advanced missions
37
+ - CLI options and configuration
38
+ - Integrating with Mission Control
39
+ - Troubleshooting & FAQs
40
+
41
+ ---
42
+
27
43
  ## 📦 Installation
28
44
 
29
45
  ```
package/bin/cli.js CHANGED
@@ -7,9 +7,14 @@ import { createWelcomeMission } from './createWelcomeMission.js';
7
7
  import { generateHtmlReport } from '../tools/generateHtmlReport.js';
8
8
  import inquirer from 'inquirer';
9
9
  import fetch from 'node-fetch';
10
+ import crypto from 'crypto';
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
 
14
+ // const apiBase = `http://localhost:3002` //
15
+ const apiBase = 'http://api.testronaut.app'; // Replace with your actual API base URL
16
+
17
+
13
18
  const args = process.argv.slice(2);
14
19
  const missionsDir = path.resolve(process.cwd(), 'missions');
15
20
 
@@ -124,6 +129,27 @@ fs.mkdirSync(outputDir, { recursive: true });
124
129
  fs.writeFileSync(`${outputDir}/${runId}.json`, JSON.stringify(report, null, 2));
125
130
  generateHtmlReport(report, `${outputDir}/${runId}.html`);
126
131
 
132
+ // helpers near top of cli.js
133
+ async function parseJsonSafe(res, urlLabel) {
134
+ const text = await res.text(); // read once
135
+ let data = null;
136
+ // console.log('response text: ', text);
137
+ // console.log('urlLabel: ', urlLabel);
138
+ // console.log('res', res);
139
+ try { data = text ? JSON.parse(text) : null; } catch { /* not JSON */ }
140
+
141
+ if (!res.ok) {
142
+ // bubble up useful diagnostics
143
+ const snippet = text?.slice(0, 300) || '(no body)';
144
+ throw new Error(`${urlLabel} ${res.status} ${res.statusText}: ${snippet}`);
145
+ }
146
+ if (!data) {
147
+ throw new Error(`${urlLabel} returned no JSON body`);
148
+ }
149
+ return data;
150
+ }
151
+
152
+
127
153
  // Function to handle login and store session token
128
154
  async function handleLogin() {
129
155
  // Check if API key is provided as an argument
@@ -149,7 +175,7 @@ async function handleLogin() {
149
175
 
150
176
  // Call your authentication endpoint to get a session token
151
177
  try {
152
- const response = await fetch('http://api.testronaut.app/api/user/cli', { // Replace with your actual URL
178
+ const response = await fetch(`${apiBase}/api/user/cli`, { // Replace with your actual URL
153
179
  method: 'POST',
154
180
  headers: {
155
181
  'Content-Type': 'application/json',
@@ -192,48 +218,156 @@ async function uploadReport() {
192
218
  console.error('❌ Configuration file not found.');
193
219
  process.exit(1);
194
220
  }
195
-
196
221
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
197
222
  const sessionToken = config.sessionToken;
198
-
199
223
  if (!sessionToken) {
200
224
  console.error('❌ No session token found.');
201
225
  process.exit(1);
202
226
  }
203
227
 
204
- // Get the most recent report from the 'missions/mission_reports/' directory
205
- const reportDir = path.resolve(process.cwd(), 'missions/mission_reports');
206
- const files = fs.readdirSync(reportDir).filter(file => file.endsWith('.json'));
207
228
 
229
+ // 1) Find latest report JSON
230
+ const reportDir = path.resolve(process.cwd(), 'missions/mission_reports');
231
+ const files = fs.readdirSync(reportDir).filter(f => f.endsWith('.json'));
208
232
  if (files.length === 0) {
209
233
  console.error('❌ No report files found.');
210
234
  process.exit(1);
211
235
  }
212
-
213
- // Sort files by timestamp (most recent first)
214
236
  files.sort((a, b) => parseInt(b.split('_')[1]) - parseInt(a.split('_')[1]));
215
- const latestReport = path.join(reportDir, files[0]);
237
+ const latestReportFile = files[0];
238
+ const latestReportPath = path.join(reportDir, latestReportFile);
239
+ const reportJson = fs.readFileSync(latestReportPath, 'utf8');
216
240
 
217
- // Read the content of the most recent report
218
- const reportData = fs.readFileSync(latestReport, 'utf8');
219
-
220
- // Upload the report using the session token
241
+ // 2) Upload report FIRST and capture its ID
242
+ console.log(`🛰️ Uploading report: ${latestReportFile}`);
243
+ let savedReportId; // <-- make sure this is defined before screenshot code
221
244
  try {
222
- const response = await fetch(`http://api.testronaut.app/api/user/cli/${sessionToken}`, { // Replace with your API endpoint
245
+ const res = await fetch(`${apiBase}/api/user/cli/${sessionToken}`, {
223
246
  method: 'POST',
224
- headers: {
225
- 'Content-Type': 'application/json',
226
- },
227
- body: reportData, // Sending the report JSON as the request body
247
+ headers: { 'Content-Type': 'application/json' },
248
+ body: reportJson,
228
249
  });
250
+ const data = await res.json();
251
+ if (!res.ok || data.error) throw new Error(data.error || res.statusText);
252
+
253
+ // Your API returns { message, report: savedReport }
254
+ savedReportId = data?.report?._id;
255
+ if (!savedReportId) {
256
+ // Fallback: use filename stem (not ideal, but prevents a hard stop)
257
+ savedReportId = path.basename(latestReportFile, '.json');
258
+ console.warn(`⚠️ Server response lacked report._id, falling back to ${savedReportId}`);
259
+ }
260
+ console.log('✅ Report uploaded:', savedReportId);
261
+ } catch (err) {
262
+ console.error('❌ Failed to upload the report:', err.message || err);
263
+ process.exit(1);
264
+ }
229
265
 
230
- const data = await response.json();
231
- if (data.error) {
232
- console.error('❌ Failed to upload the report:', data.error);
233
- } else {
234
- console.log('✅ Report uploaded successfully!');
266
+ // 3) Now find the fixed 'screenshots' folder next to JSON
267
+ const screenshotsDir = path.join(reportDir, 'screenshots');
268
+ if (!fs.existsSync(screenshotsDir) || !fs.statSync(screenshotsDir).isDirectory()) {
269
+ console.log('ℹ️ No screenshots directory found. Done.');
270
+ return;
271
+ }
272
+
273
+ // 4) Collect and sort images by timestamp in filename
274
+ const parseScreenshotTimestamp = (name) => {
275
+ const m = name.match(/^screenshot_(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/i);
276
+ if (!m) return Number.MAX_SAFE_INTEGER;
277
+ const [_, Y, M, D, h, mnt, s, ms] = m;
278
+ return Date.UTC(+Y, +M - 1, +D, +h, +mnt, +s, +ms);
279
+ };
280
+
281
+ let imageFiles = fs
282
+ .readdirSync(screenshotsDir)
283
+ .filter(f => /\.(png|jpg|jpeg|webp|gif)$/i.test(f))
284
+ .sort((a, b) => parseScreenshotTimestamp(a) - parseScreenshotTimestamp(b));
285
+
286
+ if (!imageFiles.length) {
287
+ console.log('ℹ️ Screenshots directory is empty. Done.');
288
+ return;
289
+ }
290
+
291
+ console.log(`🖼️ Uploading ${imageFiles.length} screenshot(s) from ${path.relative(process.cwd(), screenshotsDir)}`);
292
+
293
+ // 5) Upload screenshots (uses savedReportId captured above)
294
+ const guessMime = p => {
295
+ const e = path.extname(p).toLowerCase();
296
+ if (e === '.png') return 'image/png';
297
+ if (e === '.jpg' || e === '.jpeg') return 'image/jpeg';
298
+ if (e === '.webp') return 'image/webp';
299
+ if (e === '.gif') return 'image/gif';
300
+ return 'application/octet-stream';
301
+ };
302
+ const sha1 = buf => crypto.createHash('sha1').update(buf).digest('hex');
303
+
304
+ const failures = [];
305
+ for (let i = 0; i < imageFiles.length; i++) {
306
+ const f = imageFiles[i];
307
+ const filePath = path.join(screenshotsDir, f);
308
+ const stepIndex = i;
309
+
310
+ process.stdout.write(` ➜ ${f} (step ${stepIndex}) … `);
311
+ try {
312
+ const buf = fs.readFileSync(filePath);
313
+ const stat = fs.statSync(filePath);
314
+ const mime = guessMime(filePath);
315
+ const hash = sha1(buf);
316
+
317
+ // START
318
+ // console.log(' uploading', filePath);
319
+ // console.log(` ➜ hope it goes well…`);
320
+ // console.log(` the fetch url is: ${apiBase}/api/user/cli/${sessionToken}/uploads/start`);
321
+ const startRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/start`, {
322
+ method: 'POST',
323
+ headers: { 'Content-Type': 'application/json' },
324
+ body: JSON.stringify({ reportId: savedReportId, stepIndex, fileName: path.basename(filePath), mime, size: stat.size, sha1: hash }),
325
+ });
326
+ const start = await parseJsonSafe(startRes, 'uploads/start');
327
+ if (!start.putUrl || !start.key) throw new Error(`uploads/start missing putUrl/key`);
328
+
329
+ // PUT
330
+ // console.log(` uploading ${stat.size} bytes to storage…`);
331
+ const putRes = await fetch(start.putUrl, {
332
+ method: 'PUT',
333
+ headers: { 'Content-Type': mime, 'Content-Length': String(stat.size) },
334
+ body: buf,
335
+ });
336
+ if (!putRes.ok) {
337
+ const text = await putRes.text().catch(() => '');
338
+ throw new Error(`${putRes.status} ${text}`);
339
+ }
340
+
341
+ // FINISH
342
+ // console.log(` finalizing upload…`);
343
+ const finishRes = await fetch(`${apiBase}/api/user/cli/${sessionToken}/uploads/finish`, {
344
+ method: 'POST',
345
+ headers: { 'Content-Type': 'application/json' },
346
+ body: JSON.stringify({
347
+ reportId: savedReportId,
348
+ stepIndex,
349
+ key: start.key,
350
+ mime,
351
+ bytes: stat.size,
352
+ sha1: hash,
353
+ originalFileName: path.basename(filePath),
354
+ }),
355
+ });
356
+ const finish = await parseJsonSafe(finishRes, 'uploads/finish');
357
+ if (!finish.ok) throw new Error(`uploads/finish responded ok=false`);
358
+
359
+ process.stdout.write('ok\n');
360
+ } catch (err) {
361
+ process.stdout.write('FAIL\n');
362
+ failures.push({ file: f, error: err.message || String(err) });
235
363
  }
236
- } catch (error) {
237
- console.error('❌ Error during upload:', error);
238
364
  }
239
- }
365
+
366
+ if (failures.length) {
367
+ console.log('\n⚠️ Some screenshots failed to upload:');
368
+ for (const f of failures) console.log(` - ${f.file}: ${f.error}`);
369
+ process.exitCode = 1;
370
+ } else {
371
+ console.log('✅ All screenshots uploaded.');
372
+ }
373
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"