testdriverai 7.2.36 → 7.2.37
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/interfaces/cli/lib/base.js +23 -7
- package/interfaces/vitest-plugin.mjs +138 -40
- package/package.json +1 -1
|
@@ -144,19 +144,35 @@ class BaseCommand extends Command {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
// Handle process signals to ensure clean disconnection
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
157
|
+
await this.agent.exit(true, false, false);
|
|
151
158
|
} catch (err) {
|
|
152
|
-
|
|
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
|
|
@@ -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("
|
|
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
|
-
|
|
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.
|
|
432
|
-
|
|
433
|
-
|
|
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.
|
|
438
|
-
|
|
439
|
-
|
|
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("
|
|
516
|
-
logger.debug("
|
|
517
|
-
logger.debug("
|
|
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
|
|
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.
|
|
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("
|
|
584
|
-
logger.debug("
|
|
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("
|
|
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("
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
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)
|