testdriverai 7.2.36 → 7.2.38

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.
@@ -9,6 +9,10 @@ on:
9
9
  jobs:
10
10
  test:
11
11
  runs-on: ubuntu-latest
12
+
13
+ permissions:
14
+ contents: read
15
+ pull-requests: write # Required to post comments on PRs
12
16
 
13
17
  steps:
14
18
  - uses: actions/checkout@v4
@@ -25,6 +29,8 @@ jobs:
25
29
  - name: Run TestDriver.ai tests
26
30
  env:
27
31
  TD_API_KEY: ${{ secrets.TD_API_KEY }}
32
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33
+ GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
28
34
  run: npx vitest run
29
35
 
30
36
  - name: Upload test results
@@ -144,19 +144,35 @@ class BaseCommand extends Command {
144
144
  });
145
145
 
146
146
  // Handle process signals to ensure clean disconnection
147
- const cleanupAndExit = () => {
148
- if (this.agent?.sandbox) {
147
+ let isExiting = false;
148
+ const cleanupAndExit = async (signal) => {
149
+ if (isExiting) return;
150
+ isExiting = true;
151
+
152
+ console.log(`\nReceived ${signal}, cleaning up...`);
153
+
154
+ // Use the agent's exit method for proper cleanup
155
+ if (this.agent) {
149
156
  try {
150
- this.agent.sandbox.close();
157
+ await this.agent.exit(true, false, false);
151
158
  } catch (err) {
152
- // Ignore close errors
159
+ console.error("Error during cleanup:", err.message);
153
160
  }
161
+ } else {
162
+ // Fallback if no agent
163
+ if (this.agent?.sandbox) {
164
+ try {
165
+ this.agent.sandbox.close();
166
+ } catch (err) {
167
+ // Ignore close errors
168
+ }
169
+ }
170
+ process.exit(1);
154
171
  }
155
- process.exit(1);
156
172
  };
157
173
 
158
- process.on('SIGINT', cleanupAndExit);
159
- process.on('SIGTERM', cleanupAndExit);
174
+ process.on('SIGINT', () => cleanupAndExit('SIGINT'));
175
+ process.on('SIGTERM', () => cleanupAndExit('SIGTERM'));
160
176
 
161
177
  // Handle unhandled promise rejections to prevent them from interfering with the exit flow
162
178
  // This is particularly important when JavaScript execution in VM contexts leaves dangling promises
@@ -2,8 +2,8 @@ import { execSync } from "child_process";
2
2
  import crypto from "crypto";
3
3
  import { createRequire } from "module";
4
4
  import path from "path";
5
+ import { postOrUpdateTestResults } from "../lib/github-comment.mjs";
5
6
  import { setTestRunInfo } from "./shared-test-state.mjs";
6
- import { generateGitHubComment, postOrUpdateTestResults } from "../lib/github-comment.mjs";
7
7
 
8
8
  // Use createRequire to import CommonJS modules without esbuild processing
9
9
  const require = createRequire(import.meta.url);
@@ -383,11 +383,23 @@ export async function cleanupTestDriver(testdriver) {
383
383
  * Handle process termination and mark test run as cancelled
384
384
  */
385
385
  async function handleProcessExit() {
386
+ logger.debug("handleProcessExit called");
387
+ logger.debug("testRun:", !!pluginState.testRun);
388
+ logger.debug("testRunId:", pluginState.testRunId);
389
+ logger.debug("testRunCompleted:", pluginState.testRunCompleted);
390
+
386
391
  if (!pluginState.testRun || !pluginState.testRunId) {
392
+ logger.debug("No test run to cancel - skipping cleanup");
393
+ return;
394
+ }
395
+
396
+ // Prevent duplicate completion
397
+ if (pluginState.testRunCompleted) {
398
+ logger.debug("Test run already completed - skipping cancellation");
387
399
  return;
388
400
  }
389
401
 
390
- logger.debug("Process interrupted, marking test run as cancelled...");
402
+ logger.debug("Marking test run as cancelled...");
391
403
 
392
404
  try {
393
405
  const stats = {
@@ -413,8 +425,10 @@ async function handleProcessExit() {
413
425
  completeData.platform = platform;
414
426
  }
415
427
 
428
+ logger.debug("Calling completeTestRun with:", JSON.stringify(completeData));
416
429
  await completeTestRun(completeData);
417
- logger.debug("✅ Test run marked as cancelled");
430
+ pluginState.testRunCompleted = true;
431
+ logger.info("Test run marked as cancelled");
418
432
  } catch (error) {
419
433
  logger.error("Failed to mark test run as cancelled:", error.message);
420
434
  }
@@ -422,21 +436,77 @@ async function handleProcessExit() {
422
436
 
423
437
  // Set up process exit handlers
424
438
  let exitHandlersRegistered = false;
439
+ let isExiting = false;
440
+ let isCancelling = false; // Track if we're in the process of cancelling due to SIGINT/SIGTERM
425
441
 
426
442
  function registerExitHandlers() {
427
443
  if (exitHandlersRegistered) return;
428
444
  exitHandlersRegistered = true;
429
445
 
430
- // Handle Ctrl+C
431
- process.on("SIGINT", async () => {
432
- await handleProcessExit();
433
- process.exit(130); // Standard exit code for SIGINT
446
+ // Handle Ctrl+C - use 'once' and prepend to run before Vitest's handler
447
+ process.prependOnceListener("SIGINT", () => {
448
+ logger.debug("SIGINT received, cleaning up...");
449
+ if (isExiting) {
450
+ logger.debug("Already exiting, skipping duplicate handler");
451
+ return;
452
+ }
453
+ isExiting = true;
454
+ isCancelling = true; // Mark that we're cancelling
455
+
456
+ // Temporarily override process.exit to prevent Vitest from exiting before we're done
457
+ const originalExit = process.exit;
458
+ let exitCalled = false;
459
+ let exitCode = 130;
460
+
461
+ process.exit = (code) => {
462
+ if (!exitCalled) {
463
+ exitCalled = true;
464
+ exitCode = code ?? 130;
465
+ logger.debug(`process.exit(${exitCode}) called, waiting for cleanup...`);
466
+ }
467
+ };
468
+
469
+ handleProcessExit()
470
+ .then(() => {
471
+ logger.debug("Cleanup completed successfully");
472
+ })
473
+ .catch((err) => {
474
+ logger.error("Error during SIGINT cleanup:", err.message);
475
+ })
476
+ .finally(() => {
477
+ logger.debug(`Exiting with code ${exitCode}`);
478
+ // Restore and call original exit
479
+ process.exit = originalExit;
480
+ process.exit(exitCode);
481
+ });
434
482
  });
435
483
 
436
484
  // Handle kill command
437
- process.on("SIGTERM", async () => {
438
- await handleProcessExit();
439
- process.exit(143); // Standard exit code for SIGTERM
485
+ process.prependOnceListener("SIGTERM", () => {
486
+ logger.debug("SIGTERM received, cleaning up...");
487
+ if (isExiting) return;
488
+ isExiting = true;
489
+ isCancelling = true;
490
+
491
+ const originalExit = process.exit;
492
+ let exitCode = 143;
493
+
494
+ process.exit = (code) => {
495
+ exitCode = code ?? 143;
496
+ };
497
+
498
+ handleProcessExit()
499
+ .then(() => {
500
+ logger.debug("Cleanup completed successfully");
501
+ })
502
+ .catch((err) => {
503
+ logger.error("Error during SIGTERM cleanup:", err.message);
504
+ })
505
+ .finally(() => {
506
+ logger.debug(`Exiting with code ${exitCode}`);
507
+ process.exit = originalExit;
508
+ process.exit(exitCode);
509
+ });
440
510
  });
441
511
 
442
512
  }
@@ -512,9 +582,9 @@ class TestDriverReporter {
512
582
  }
513
583
 
514
584
  async initializeTestRun() {
515
- logger.debug("Initializing test run...");
516
- logger.debug("Current API key in pluginState:", !!pluginState.apiKey);
517
- logger.debug("Current API root in pluginState:", pluginState.apiRoot);
585
+ logger.debug("initializeTestRun called");
586
+ logger.debug("API key present:", !!pluginState.apiKey);
587
+ logger.debug("API root:", pluginState.apiRoot);
518
588
 
519
589
  // Check if we should enable the reporter
520
590
  if (!pluginState.apiKey) {
@@ -552,9 +622,9 @@ class TestDriverReporter {
552
622
  // Default to linux if no tests write platform info
553
623
  testRunData.platform = "linux";
554
624
 
555
- logger.debug("Creating test run with data:", testRunData);
625
+ logger.debug("Creating test run with data:", JSON.stringify(testRunData));
556
626
  pluginState.testRun = await createTestRun(testRunData);
557
- logger.debug("Test run created successfully:", pluginState.testRun);
627
+ logger.debug("Test run created:", JSON.stringify(pluginState.testRun));
558
628
 
559
629
  // Store in environment variables for worker processes to access
560
630
  process.env.TD_TEST_RUN_ID = pluginState.testRunId;
@@ -571,7 +641,7 @@ class TestDriverReporter {
571
641
  startTime: pluginState.startTime,
572
642
  });
573
643
 
574
- logger.debug(`Test run created: ${pluginState.testRunId}`);
644
+ logger.info(`Test run created: ${pluginState.testRunId}`);
575
645
  } catch (error) {
576
646
  logger.error("Failed to initialize:", error.message);
577
647
  pluginState.apiKey = null;
@@ -580,8 +650,24 @@ class TestDriverReporter {
580
650
  }
581
651
 
582
652
  async onTestRunEnd(testModules, unhandledErrors, reason) {
583
- logger.debug("Test run ending with reason:", reason);
584
- logger.debug("Plugin state - API key present:", !!pluginState.apiKey, "Test run present:", !!pluginState.testRun);
653
+ logger.debug("onTestRunEnd called with reason:", reason);
654
+ logger.debug("API key present:", !!pluginState.apiKey);
655
+ logger.debug("Test run present:", !!pluginState.testRun);
656
+ logger.debug("Test run ID:", pluginState.testRunId);
657
+ logger.debug("isCancelling:", isCancelling);
658
+ logger.debug("testRunCompleted:", pluginState.testRunCompleted);
659
+
660
+ // If we're cancelling due to SIGINT/SIGTERM, skip - handleProcessExit will handle it
661
+ if (isCancelling) {
662
+ logger.debug("Cancellation in progress via signal handler, skipping onTestRunEnd");
663
+ return;
664
+ }
665
+
666
+ // If already completed (by handleProcessExit), skip
667
+ if (pluginState.testRunCompleted) {
668
+ logger.debug("Test run already completed, skipping");
669
+ return;
670
+ }
585
671
 
586
672
  if (!pluginState.apiKey) {
587
673
  logger.warn("Skipping completion - no API key (was it cleared after init failure?)");
@@ -644,10 +730,11 @@ class TestDriverReporter {
644
730
  }
645
731
 
646
732
  // Test cases are reported directly from teardownTest
647
- logger.debug("All test cases reported from teardown");
733
+ logger.debug("Calling completeTestRun API...");
734
+ logger.debug("Complete data:", JSON.stringify(completeData));
648
735
 
649
736
  const completeResponse = await completeTestRun(completeData);
650
- logger.debug("Test run completion API response:", completeResponse);
737
+ logger.debug("API response:", JSON.stringify(completeResponse));
651
738
 
652
739
  // Mark test run as completed to prevent duplicate completion
653
740
  pluginState.testRunCompleted = true;
@@ -657,7 +744,7 @@ class TestDriverReporter {
657
744
  const consoleUrl = getConsoleUrl(pluginState.apiRoot);
658
745
  if (testRunDbId) {
659
746
  const testRunUrl = `${consoleUrl}/runs/${testRunDbId}`;
660
- logger.debug(`🔗 View test run: ${testRunUrl}`);
747
+ logger.info(`View test run: ${testRunUrl}`);
661
748
  // Output in a parseable format for CI
662
749
  console.log(`TESTDRIVER_RUN_URL=${testRunUrl}`);
663
750
 
@@ -665,7 +752,7 @@ class TestDriverReporter {
665
752
  await postGitHubCommentIfEnabled(testRunUrl, stats, completeData);
666
753
  }
667
754
 
668
- logger.debug(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
755
+ logger.info(`Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
669
756
  } catch (error) {
670
757
  logger.error("Failed to complete test run:", error.message);
671
758
  logger.debug("Error stack:", error.stack);
@@ -1134,27 +1221,38 @@ async function createTestRun(data) {
1134
1221
 
1135
1222
  async function completeTestRun(data) {
1136
1223
  const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-complete`;
1137
- const response = await withTimeout(
1138
- fetch(url, {
1139
- method: "POST",
1140
- headers: {
1141
- "Content-Type": "application/json",
1142
- Authorization: `Bearer ${pluginState.token}`,
1143
- },
1144
- body: JSON.stringify(data),
1145
- }),
1146
- 10000,
1147
- "Internal Complete Test Run",
1148
- );
1149
-
1150
- if (!response.ok) {
1151
- const errorText = await response.text();
1152
- throw new Error(
1153
- `API error: ${response.status} ${response.statusText} - ${errorText}`,
1224
+ logger.debug(`completeTestRun: POSTing to ${url}`);
1225
+
1226
+ try {
1227
+ const response = await withTimeout(
1228
+ fetch(url, {
1229
+ method: "POST",
1230
+ headers: {
1231
+ "Content-Type": "application/json",
1232
+ Authorization: `Bearer ${pluginState.token}`,
1233
+ },
1234
+ body: JSON.stringify(data),
1235
+ }),
1236
+ 10000,
1237
+ "Internal Complete Test Run",
1154
1238
  );
1155
- }
1156
1239
 
1157
- return await response.json();
1240
+ logger.debug(`completeTestRun: Response status ${response.status}`);
1241
+
1242
+ if (!response.ok) {
1243
+ const errorText = await response.text();
1244
+ throw new Error(
1245
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
1246
+ );
1247
+ }
1248
+
1249
+ const result = await response.json();
1250
+ logger.debug(`completeTestRun: Success`);
1251
+ return result;
1252
+ } catch (error) {
1253
+ logger.error(`completeTestRun: Error - ${error.message}`);
1254
+ throw error;
1255
+ }
1158
1256
  }
1159
1257
 
1160
1258
  // Global state setup moved to setup file (vitestSetup.mjs)
@@ -47,10 +47,17 @@ function generateTestResultsTable(testCases, testRunUrl) {
47
47
  return '_No test cases recorded_';
48
48
  }
49
49
 
50
+ // Filter out skipped tests
51
+ const nonSkippedTests = testCases.filter(test => test.status !== 'skipped');
52
+
53
+ if (nonSkippedTests.length === 0) {
54
+ return '_No test cases to display (all tests were skipped)_';
55
+ }
56
+
50
57
  let table = '| Status | Test | File | Duration | Replay |\n';
51
58
  table += '|--------|------|------|----------|--------|\n';
52
59
 
53
- for (const test of testCases) {
60
+ for (const test of nonSkippedTests) {
54
61
  const status = getStatusEmoji(test.status);
55
62
  const name = test.testName || 'Unknown';
56
63
  const file = test.testFile || 'unknown';
@@ -65,8 +72,8 @@ function generateTestResultsTable(testCases, testRunUrl) {
65
72
  const replayId = extractReplayId(test.replayUrl);
66
73
  if (replayId) {
67
74
  const gifUrl = getReplayGifUrl(test.replayUrl, replayId);
68
- // Embed GIF with link using HTML for width control
69
- replay = `<a href="${linkUrl}"><img src="${gifUrl}" width="250" alt="${name}" /></a>`;
75
+ // Embed GIF with link using HTML for height control
76
+ replay = `<a href="${linkUrl}"><img src="${gifUrl}" height="100" alt="Test replay" /></a>`;
70
77
  } else {
71
78
  // Fallback to text link if no GIF available
72
79
  replay = `[🎥 View](${linkUrl})`;
@@ -258,9 +265,15 @@ export function generateGitHubComment(testRunData, testCases = []) {
258
265
  comment += ` • **Duration:** ${formatDuration(duration)}`;
259
266
  comment += ` • ${passedTests} passed`;
260
267
  if (failedTests > 0) comment += `, ${failedTests} failed`;
261
- if (skippedTests > 0) comment += `, ${skippedTests} skipped`;
268
+ // Only show skipped count if there are no passed or failed tests
269
+ if (skippedTests > 0 && passedTests === 0 && failedTests === 0) {
270
+ comment += `, ${skippedTests} skipped`;
271
+ }
262
272
  comment += `\n\n`;
263
273
 
274
+ // Exceptions section (only if there are failures) - show first
275
+ comment += generateExceptionsSection(testCases, testRunUrl);
276
+
264
277
  // Test results table (now includes embedded GIFs)
265
278
  comment += '## 📝 Test Results\n\n';
266
279
  comment += generateTestResultsTable(testCases, testRunUrl);
@@ -270,9 +283,6 @@ export function generateGitHubComment(testRunData, testCases = []) {
270
283
  comment += `\n[📋 View Full Test Run](${testRunUrl})\n`;
271
284
  }
272
285
 
273
- // Exceptions section (only if there are failures)
274
- comment += generateExceptionsSection(testCases, testRunUrl);
275
-
276
286
  // Footer
277
287
  comment += '\n---\n';
278
288
  comment += `<sub>Generated by [TestDriver](https://testdriver.ai) • Run ID: \`${runId}\`</sub>\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.36",
3
+ "version": "7.2.38",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "exports": {
@@ -1,73 +0,0 @@
1
- name: TestDriver Tests with GitHub Comments
2
-
3
- on:
4
- pull_request:
5
- types: [opened, synchronize, reopened]
6
- push:
7
- branches: [main]
8
- workflow_dispatch: # Allows manual trigger from Actions tab
9
-
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
-
14
- permissions:
15
- contents: read
16
- pull-requests: write # Required to post comments on PRs
17
-
18
- steps:
19
- - name: Checkout code
20
- uses: actions/checkout@v4
21
-
22
- - name: Setup Node.js
23
- uses: actions/setup-node@v4
24
- with:
25
- node-version: '20'
26
- cache: 'npm'
27
-
28
- - name: Install dependencies
29
- run: npm ci
30
-
31
- - name: Run assert test with GitHub comments
32
- env:
33
- # TestDriver API key (from repository secrets)
34
- TD_API_KEY: ${{ secrets.TD_API_KEY }}
35
-
36
- # GitHub token for posting comments (auto-provided)
37
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38
-
39
- # PR number for PR comments (auto-extracted for pull_request events)
40
- GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
41
-
42
- # Git information (automatically provided by GitHub Actions)
43
- # GITHUB_SHA, GITHUB_REF, GITHUB_REPOSITORY are already set
44
-
45
- run: |
46
- echo "🚀 Running TestDriver assert test..."
47
- echo "📍 Repository: $GITHUB_REPOSITORY"
48
- echo "🔢 PR Number: ${{ github.event.pull_request.number || 'N/A (not a PR)' }}"
49
- echo "📦 Running test..."
50
- npm run test:sdk -- test/testdriver/assert.test.mjs
51
-
52
- - name: Upload test results (on failure)
53
- if: failure()
54
- uses: actions/upload-artifact@v4
55
- with:
56
- name: test-results
57
- path: |
58
- test-report.junit.xml
59
- .testdriver/
60
- retention-days: 7
61
-
62
- - name: Comment on PR (manual fallback if auto-comment fails)
63
- if: failure() && github.event_name == 'pull_request'
64
- uses: actions/github-script@v7
65
- with:
66
- script: |
67
- const testRunUrl = process.env.TESTDRIVER_RUN_URL || 'Check workflow logs';
68
- github.rest.issues.createComment({
69
- issue_number: context.issue.number,
70
- owner: context.repo.owner,
71
- repo: context.repo.repo,
72
- body: `⚠️ TestDriver tests encountered an error. ${testRunUrl}`
73
- })