endorphin-ai 0.1.0

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 (35) hide show
  1. package/LICENSE.md +209 -0
  2. package/README.md +474 -0
  3. package/bin/endorphin.js +256 -0
  4. package/examples/endorphin.config.js +22 -0
  5. package/examples/tests/QE-001-basic-login-test.js +18 -0
  6. package/examples/tests/sample-test.js +9 -0
  7. package/examples/tools/.gitkeep +0 -0
  8. package/framework/config/agent-config.js +53 -0
  9. package/framework/config/browser-config.js +53 -0
  10. package/framework/config/paths.js +40 -0
  11. package/framework/core/agent-setup.js +75 -0
  12. package/framework/core/browser-framework.js +766 -0
  13. package/framework/core/config-loader.js +309 -0
  14. package/framework/core/test-discovery.js +402 -0
  15. package/framework/core/test-manager.js +343 -0
  16. package/framework/core/test-recorder.js +302 -0
  17. package/framework/core/test-runner.js +133 -0
  18. package/framework/core/test-session.js +98 -0
  19. package/framework/index.js +44 -0
  20. package/framework/interactive/enhanced-interactive-recorder.js +223 -0
  21. package/framework/interactive/index.js +18 -0
  22. package/framework/interactive/interactive-test-clean.js +33 -0
  23. package/framework/interactive/interactive-test.js +33 -0
  24. package/framework/test-framework.js +29 -0
  25. package/framework/testing/index.js +15 -0
  26. package/framework/testing/test-interactive-recorder.js +83 -0
  27. package/framework/testing/test-modular-framework.js +47 -0
  28. package/framework/testing/verify-test-format.js +58 -0
  29. package/framework/tools/content.js +67 -0
  30. package/framework/tools/index.js +52 -0
  31. package/framework/tools/interaction.js +180 -0
  32. package/framework/tools/navigation.js +43 -0
  33. package/framework/tools/utilities.js +99 -0
  34. package/framework/tools/verification.js +84 -0
  35. package/package.json +84 -0
@@ -0,0 +1,309 @@
1
+ // Configuration loader for Endorphin AI
2
+ // Handles loading and merging of configuration from multiple sources
3
+
4
+ import { existsSync } from 'fs';
5
+ import { resolve, join } from 'path';
6
+ import { pathToFileURL } from 'url';
7
+
8
+ export class ConfigLoader {
9
+ constructor() {
10
+ this.defaultConfig = {
11
+ // Browser Configuration
12
+ browser: {
13
+ headless: false,
14
+ viewport: { width: 1280, height: 720 },
15
+ timeout: 30000,
16
+ slowMo: 0,
17
+ deviceScaleFactor: 1,
18
+ },
19
+
20
+ // AI Configuration
21
+ ai: {
22
+ model: "gpt-4o",
23
+ maxRetries: 3,
24
+ temperature: 0.1,
25
+ maxTokens: 4000,
26
+ },
27
+
28
+ // Test Execution Settings
29
+ execution: {
30
+ screenshots: true,
31
+ recordVideo: false,
32
+ pauseOnError: false,
33
+ continueOnError: false,
34
+ maxConcurrency: 1,
35
+ },
36
+
37
+ // Global Test Data
38
+ testData: {
39
+ baseUrl: "https://example.com",
40
+ timeout: 5000,
41
+ retryCount: 2,
42
+ },
43
+
44
+ // Result Storage
45
+ results: {
46
+ directory: "./test-results",
47
+ keepHistory: 10,
48
+ format: ["json", "html"],
49
+ },
50
+
51
+ // Environment Settings
52
+ environments: {
53
+ development: {
54
+ baseUrl: "http://localhost:3000",
55
+ headless: false,
56
+ },
57
+ staging: {
58
+ baseUrl: "https://staging.example.com",
59
+ headless: true,
60
+ },
61
+ production: {
62
+ baseUrl: "https://example.com",
63
+ headless: true,
64
+ screenshots: false,
65
+ }
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Load configuration from all sources and merge them
72
+ * Priority: CLI flags > User config > Environment > Defaults
73
+ */
74
+ async loadConfig(options = {}) {
75
+ let config = { ...this.defaultConfig };
76
+
77
+ // 1. Load user config file if it exists
78
+ const userConfig = await this.loadUserConfig();
79
+ if (userConfig) {
80
+ config = this.mergeConfig(config, userConfig);
81
+ }
82
+
83
+ // 2. Apply environment variables
84
+ const envConfig = this.loadEnvironmentConfig();
85
+ config = this.mergeConfig(config, envConfig);
86
+
87
+ // 3. Apply CLI options (highest priority)
88
+ if (options) {
89
+ // Transform CLI flags to proper config structure
90
+ const transformedOptions = this.transformCliFlags(options);
91
+ config = this.mergeConfig(config, transformedOptions);
92
+ }
93
+
94
+ // 4. Apply environment-specific overrides
95
+ const environment = process.env.NODE_ENV || 'development';
96
+ if (config.environments && config.environments[environment]) {
97
+ config = this.mergeConfig(config, config.environments[environment]);
98
+ }
99
+
100
+ return config;
101
+ }
102
+
103
+ /**
104
+ * Load user's endorphin.config.js file
105
+ */
106
+ async loadUserConfig() {
107
+ const configPaths = [
108
+ resolve(process.cwd(), 'endorphin.config.js'),
109
+ resolve(process.cwd(), 'endorphin.config.mjs'),
110
+ resolve(process.cwd(), '.endorphin.config.js'),
111
+ ];
112
+
113
+ for (const configPath of configPaths) {
114
+ if (existsSync(configPath)) {
115
+ try {
116
+ const configUrl = pathToFileURL(configPath).href;
117
+ const module = await import(`${configUrl}?t=${Date.now()}`);
118
+ const config = module.default || module;
119
+
120
+ console.log(`๐Ÿ“ Loaded config from: ${configPath}`);
121
+ return config;
122
+ } catch (error) {
123
+ console.warn(`โš ๏ธ Failed to load config from ${configPath}:`, error.message);
124
+ }
125
+ }
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Load configuration from environment variables
133
+ */
134
+ loadEnvironmentConfig() {
135
+ const envConfig = {};
136
+
137
+ // Browser settings
138
+ if (process.env.ENDORPHIN_HEADLESS !== undefined) {
139
+ envConfig.browser = { headless: process.env.ENDORPHIN_HEADLESS === 'true' };
140
+ }
141
+
142
+ if (process.env.ENDORPHIN_VIEWPORT_WIDTH || process.env.ENDORPHIN_VIEWPORT_HEIGHT) {
143
+ envConfig.browser = envConfig.browser || {};
144
+ envConfig.browser.viewport = {
145
+ width: parseInt(process.env.ENDORPHIN_VIEWPORT_WIDTH) || 1280,
146
+ height: parseInt(process.env.ENDORPHIN_VIEWPORT_HEIGHT) || 720,
147
+ };
148
+ }
149
+
150
+ // AI settings
151
+ if (process.env.ENDORPHIN_AI_MODEL) {
152
+ envConfig.ai = { model: process.env.ENDORPHIN_AI_MODEL };
153
+ }
154
+
155
+ if (process.env.ENDORPHIN_AI_TEMPERATURE) {
156
+ envConfig.ai = envConfig.ai || {};
157
+ envConfig.ai.temperature = parseFloat(process.env.ENDORPHIN_AI_TEMPERATURE);
158
+ }
159
+
160
+ // Test data
161
+ if (process.env.ENDORPHIN_BASE_URL) {
162
+ envConfig.testData = { baseUrl: process.env.ENDORPHIN_BASE_URL };
163
+ }
164
+
165
+ return envConfig;
166
+ }
167
+
168
+ /**
169
+ * Transform CLI flags to proper config structure
170
+ */
171
+ transformCliFlags(options) {
172
+ const result = { ...options };
173
+
174
+ // Extract cliFlags if present
175
+ if (options.cliFlags) {
176
+ const { cliFlags, ...otherOptions } = options;
177
+
178
+ // Map CLI flags to config structure
179
+ const configFromFlags = {};
180
+
181
+ if (cliFlags.headless !== undefined) {
182
+ configFromFlags.browser = { ...configFromFlags.browser, headless: cliFlags.headless };
183
+ }
184
+
185
+ if (cliFlags.timeout !== undefined) {
186
+ configFromFlags.execution = { ...configFromFlags.execution, timeout: cliFlags.timeout };
187
+ }
188
+
189
+ if (cliFlags.viewport !== undefined) {
190
+ configFromFlags.browser = {
191
+ ...configFromFlags.browser,
192
+ viewport: cliFlags.viewport
193
+ };
194
+ }
195
+
196
+ if (cliFlags.parallel !== undefined) {
197
+ configFromFlags.execution = {
198
+ ...configFromFlags.execution,
199
+ parallel: cliFlags.parallel
200
+ };
201
+ }
202
+
203
+ if (cliFlags.model !== undefined) {
204
+ configFromFlags.ai = { ...configFromFlags.ai, model: cliFlags.model };
205
+ }
206
+
207
+ if (cliFlags.environment !== undefined) {
208
+ configFromFlags.environment = cliFlags.environment;
209
+ }
210
+
211
+ // Merge transformed flags with other options
212
+ return this.mergeConfig(otherOptions, configFromFlags);
213
+ }
214
+
215
+ return result;
216
+ }
217
+
218
+ /**
219
+ * Deep merge two configuration objects
220
+ */
221
+ mergeConfig(target, source) {
222
+ const result = { ...target };
223
+
224
+ for (const key in source) {
225
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
226
+ result[key] = this.mergeConfig(result[key] || {}, source[key]);
227
+ } else {
228
+ result[key] = source[key];
229
+ }
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ /**
236
+ * Validate configuration
237
+ */
238
+ validateConfig(config) {
239
+ const errors = [];
240
+
241
+ // Validate browser config
242
+ if (config.browser) {
243
+ if (config.browser.viewport) {
244
+ if (!config.browser.viewport.width || !config.browser.viewport.height) {
245
+ errors.push('Browser viewport must have width and height');
246
+ }
247
+ }
248
+ }
249
+
250
+ // Validate AI config
251
+ if (config.ai) {
252
+ if (config.ai.temperature !== undefined) {
253
+ if (config.ai.temperature < 0 || config.ai.temperature > 1) {
254
+ errors.push('AI temperature must be between 0 and 1');
255
+ }
256
+ }
257
+ }
258
+
259
+ if (errors.length > 0) {
260
+ throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
261
+ }
262
+
263
+ return true;
264
+ }
265
+
266
+ /**
267
+ * Get configuration value by path (e.g., 'browser.viewport.width')
268
+ */
269
+ getConfigValue(config, path) {
270
+ return path.split('.').reduce((obj, key) => obj && obj[key], config);
271
+ }
272
+
273
+ /**
274
+ * Set configuration value by path
275
+ */
276
+ setConfigValue(config, path, value) {
277
+ const keys = path.split('.');
278
+ const lastKey = keys.pop();
279
+ const target = keys.reduce((obj, key) => {
280
+ if (!obj[key]) obj[key] = {};
281
+ return obj[key];
282
+ }, config);
283
+
284
+ target[lastKey] = value;
285
+ return config;
286
+ }
287
+ }
288
+
289
+ // Global config instance
290
+ let configInstance = null;
291
+
292
+ /**
293
+ * Get the global configuration instance
294
+ */
295
+ export async function getConfig(options = {}) {
296
+ if (!configInstance) {
297
+ const loader = new ConfigLoader();
298
+ configInstance = await loader.loadConfig(options);
299
+ loader.validateConfig(configInstance);
300
+ }
301
+ return configInstance;
302
+ }
303
+
304
+ /**
305
+ * Reset the global configuration (useful for testing)
306
+ */
307
+ export function resetConfig() {
308
+ configInstance = null;
309
+ }
@@ -0,0 +1,402 @@
1
+ // Test Discovery Module
2
+ // Discovers and loads tests from user's tests/ directory
3
+
4
+ import { readdir, stat } from 'fs/promises';
5
+ import { join, resolve } from 'path';
6
+ import { pathToFileURL } from 'url';
7
+
8
+ /**
9
+ * Check if we're running in test environment
10
+ */
11
+ function isTestEnvironment() {
12
+ return process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
13
+ }
14
+
15
+ /**
16
+ * Safe exit that doesn't break tests
17
+ */
18
+ function safeExit(code) {
19
+ if (isTestEnvironment()) {
20
+ throw new Error(`process.exit called with code ${code}`);
21
+ } else {
22
+ process.exit(code);
23
+ }
24
+ }
25
+
26
+ export class TestDiscovery {
27
+ constructor() {
28
+ this.tests = new Map();
29
+ this.testsDirectory = resolve(process.cwd(), 'tests');
30
+ }
31
+
32
+ /**
33
+ * Discover and load all test files from tests/ directory
34
+ */
35
+ async discoverTests() {
36
+ try {
37
+ // Check if tests directory exists
38
+ try {
39
+ await stat(this.testsDirectory);
40
+ } catch (error) {
41
+ console.log(`๐Ÿ“ Tests directory not found: ${this.testsDirectory}`);
42
+ console.log('๐Ÿ’ก Create a "tests/" directory and add your test files there.');
43
+ return;
44
+ }
45
+
46
+ console.log(`๐Ÿ” Discovering tests in: ${this.testsDirectory}`);
47
+
48
+ const files = await readdir(this.testsDirectory);
49
+ const testFiles = files.filter(file =>
50
+ file.endsWith('.js') || file.endsWith('.mjs')
51
+ );
52
+
53
+ if (testFiles.length === 0) {
54
+ console.log('๐Ÿ“ No test files found in tests/ directory');
55
+ console.log('๐Ÿ’ก Add .js or .mjs files with exported test objects');
56
+ return;
57
+ }
58
+
59
+ console.log(`๐Ÿ“‹ Found ${testFiles.length} test file(s):`);
60
+
61
+ for (const file of testFiles) {
62
+ console.log(` ๐Ÿ“„ ${file}`);
63
+ await this.loadTestFile(file);
64
+ }
65
+
66
+ console.log(`โœ… Loaded ${this.tests.size} test(s) total\n`);
67
+
68
+ } catch (error) {
69
+ console.error('โŒ Error discovering tests:', error.message);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Load a specific test file and extract test objects
75
+ */
76
+ async loadTestFile(filename) {
77
+ try {
78
+ const filePath = join(this.testsDirectory, filename);
79
+ const fileUrl = pathToFileURL(filePath).href;
80
+
81
+ // Dynamic import with cache busting
82
+ const module = await import(`${fileUrl}?t=${Date.now()}`);
83
+
84
+ // Extract all exported test objects
85
+ for (const [exportName, exportValue] of Object.entries(module)) {
86
+ if (this.isValidTest(exportValue)) {
87
+ this.tests.set(exportValue.id, {
88
+ ...exportValue,
89
+ sourceFile: filename,
90
+ exportName
91
+ });
92
+ console.log(` โœ“ ${exportValue.id}: ${exportValue.name}`);
93
+ }
94
+ }
95
+ } catch (error) {
96
+ console.error(`โŒ Error loading ${filename}:`, error.message);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Validate if an object is a valid test
102
+ */
103
+ isValidTest(obj) {
104
+ return (
105
+ obj &&
106
+ typeof obj === 'object' &&
107
+ typeof obj.id === 'string' &&
108
+ typeof obj.name === 'string' &&
109
+ (typeof obj.task === 'string' || typeof obj.execute === 'function')
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Get test by ID
115
+ */
116
+ getTest(id) {
117
+ return this.tests.get(id);
118
+ }
119
+
120
+ /**
121
+ * Get all tests
122
+ */
123
+ getAllTests() {
124
+ return Array.from(this.tests.values());
125
+ }
126
+
127
+ /**
128
+ * Get tests by tag
129
+ */
130
+ getTestsByTag(tag) {
131
+ return this.getAllTests().filter(test =>
132
+ test.tags && test.tags.includes(tag)
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Get tests by priority
138
+ */
139
+ getTestsByPriority(priority) {
140
+ return this.getAllTests().filter(test =>
141
+ test.priority === priority
142
+ );
143
+ }
144
+
145
+ /**
146
+ * List all available tests
147
+ */
148
+ listTests() {
149
+ const tests = this.getAllTests();
150
+
151
+ if (tests.length === 0) {
152
+ console.log('๐Ÿ“ No tests found');
153
+ console.log('๐Ÿ’ก Create test files in the tests/ directory');
154
+ return;
155
+ }
156
+
157
+ console.log('\n๐Ÿ“‹ Available Tests:');
158
+ console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
159
+
160
+ const grouped = {};
161
+ tests.forEach(test => {
162
+ const priority = test.priority || 'Unknown';
163
+ if (!grouped[priority]) grouped[priority] = [];
164
+ grouped[priority].push(test);
165
+ });
166
+
167
+ for (const [priority, priorityTests] of Object.entries(grouped)) {
168
+ console.log(`\n๐ŸŽฏ ${priority} Priority:`);
169
+ priorityTests.forEach(test => {
170
+ const tags = test.tags ? `[${test.tags.join(', ')}]` : '';
171
+ console.log(` ${test.id}: ${test.name} ${tags}`);
172
+ console.log(` ๐Ÿ“„ File: ${test.sourceFile}`);
173
+ if (test.description) {
174
+ console.log(` ๐Ÿ“ ${test.description}`);
175
+ }
176
+ });
177
+ }
178
+ console.log('');
179
+ }
180
+ }
181
+
182
+ // Standalone functions for CLI usage
183
+ let discoveryInstance = null;
184
+
185
+ async function ensureDiscovery(config = null) {
186
+ if (!discoveryInstance || config) {
187
+ discoveryInstance = new TestDiscovery();
188
+ if (config?.execution?.testsDirectory) {
189
+ discoveryInstance.testsDirectory = resolve(process.cwd(), config.execution.testsDirectory);
190
+ }
191
+ await discoveryInstance.discoverTests();
192
+ }
193
+ return discoveryInstance;
194
+ }
195
+
196
+ export async function runSingleTestById(testId, config = null) {
197
+ const discovery = await ensureDiscovery(config);
198
+ const test = discovery.getTest(testId);
199
+
200
+ if (!test) {
201
+ console.error(`โŒ Test not found: ${testId}`);
202
+ console.log('๐Ÿ’ก Use "endorphin list" to see available tests');
203
+ if (isTestEnvironment()) {
204
+ return { success: false, message: `Test not found: ${testId}` };
205
+ }
206
+ safeExit(1);
207
+ }
208
+
209
+ if (isTestEnvironment()) {
210
+ return { success: true, test };
211
+ }
212
+
213
+ console.log(`๐Ÿงช Running test: ${test.id} - ${test.name}`);
214
+
215
+ const { EnhancedBrowserTestFramework } = await import('./browser-framework.js');
216
+ const framework = new EnhancedBrowserTestFramework(config);
217
+
218
+ try {
219
+ await framework.initialize();
220
+ const result = await framework.runSingleTest(test);
221
+ console.log(`โœ… Test completed: ${result.status}`);
222
+ return { success: true, result };
223
+ } catch (error) {
224
+ console.error('โŒ Test failed:', error.message);
225
+ if (isTestEnvironment()) {
226
+ return { success: false, error: error.message };
227
+ }
228
+ safeExit(1);
229
+ } finally {
230
+ await framework.cleanup();
231
+ }
232
+ }
233
+
234
+ export async function runTestsByTag(tag, config = null) {
235
+ const discovery = await ensureDiscovery(config);
236
+ const tests = discovery.getTestsByTag(tag);
237
+
238
+ if (tests.length === 0) {
239
+ console.error(`โŒ No tests found with tag: ${tag}`);
240
+ if (isTestEnvironment()) {
241
+ return { success: false, message: `No tests found with tag: ${tag}` };
242
+ }
243
+ safeExit(1);
244
+ }
245
+
246
+ if (isTestEnvironment()) {
247
+ return { success: true, tests };
248
+ }
249
+
250
+ console.log(`๐Ÿท๏ธ Running ${tests.length} test(s) with tag: ${tag}`);
251
+
252
+ const { EnhancedBrowserTestFramework } = await import('./browser-framework.js');
253
+ const framework = new EnhancedBrowserTestFramework(config);
254
+
255
+ try {
256
+ await framework.initialize();
257
+
258
+ const results = [];
259
+ for (const test of tests) {
260
+ console.log(`\n๐Ÿงช Running: ${test.id} - ${test.name}`);
261
+ const result = await framework.runSingleTest(test);
262
+ console.log(`โœ… Result: ${result.status}`);
263
+ results.push(result);
264
+ }
265
+ return { success: true, results };
266
+ } catch (error) {
267
+ console.error('โŒ Test execution failed:', error.message);
268
+ if (isTestEnvironment()) {
269
+ return { success: false, error: error.message };
270
+ }
271
+ safeExit(1);
272
+ } finally {
273
+ await framework.cleanup();
274
+ }
275
+ }
276
+
277
+ export async function runTestsByPriority(priority, config = null) {
278
+ const discovery = await ensureDiscovery(config);
279
+ const tests = discovery.getTestsByPriority(priority);
280
+
281
+ if (tests.length === 0) {
282
+ console.error(`โŒ No tests found with priority: ${priority}`);
283
+ if (isTestEnvironment()) {
284
+ return { success: false, message: `No tests found with priority: ${priority}` };
285
+ }
286
+ safeExit(1);
287
+ }
288
+
289
+ if (isTestEnvironment()) {
290
+ return { success: true, tests };
291
+ }
292
+
293
+ console.log(`๐ŸŽฏ Running ${tests.length} test(s) with priority: ${priority}`);
294
+
295
+ const { EnhancedBrowserTestFramework } = await import('./browser-framework.js');
296
+ const framework = new EnhancedBrowserTestFramework(config);
297
+
298
+ try {
299
+ await framework.initialize();
300
+
301
+ const results = [];
302
+ for (const test of tests) {
303
+ console.log(`\n๐Ÿงช Running: ${test.id} - ${test.name}`);
304
+ const result = await framework.runSingleTest(test);
305
+ console.log(`โœ… Result: ${result.status}`);
306
+ results.push(result);
307
+ }
308
+ return { success: true, results };
309
+ } catch (error) {
310
+ console.error('โŒ Test execution failed:', error.message);
311
+ if (isTestEnvironment()) {
312
+ return { success: false, error: error.message };
313
+ }
314
+ safeExit(1);
315
+ } finally {
316
+ await framework.cleanup();
317
+ }
318
+ }
319
+
320
+ export async function runAllTests(config = null) {
321
+ const discovery = await ensureDiscovery(config);
322
+ const tests = discovery.getAllTests();
323
+
324
+ if (tests.length === 0) {
325
+ console.error('โŒ No tests found');
326
+ if (isTestEnvironment()) {
327
+ return { success: false, message: 'No tests found' };
328
+ }
329
+ safeExit(1);
330
+ }
331
+
332
+ if (isTestEnvironment()) {
333
+ return { success: true, tests };
334
+ }
335
+
336
+ console.log(`๐Ÿš€ Running all ${tests.length} test(s)`);
337
+
338
+ const { EnhancedBrowserTestFramework } = await import('./browser-framework.js');
339
+ const framework = new EnhancedBrowserTestFramework(config);
340
+
341
+ try {
342
+ await framework.initialize();
343
+
344
+ let passed = 0;
345
+ let failed = 0;
346
+
347
+ for (const test of tests) {
348
+ console.log(`\n๐Ÿงช Running: ${test.id} - ${test.name}`);
349
+ try {
350
+ const result = await framework.runSingleTest(test);
351
+ console.log(`โœ… Result: ${result.status}`);
352
+ passed++;
353
+ } catch (error) {
354
+ console.error(`โŒ Failed: ${error.message}`);
355
+ failed++;
356
+ }
357
+ }
358
+
359
+ console.log(`\n๐Ÿ“Š Test Summary: ${passed} passed, ${failed} failed`);
360
+ return { success: true, passed, failed, total: tests.length };
361
+
362
+ } catch (error) {
363
+ console.error('โŒ Test execution failed:', error.message);
364
+ if (isTestEnvironment()) {
365
+ return { success: false, error: error.message };
366
+ }
367
+ safeExit(1);
368
+ } finally {
369
+ await framework.cleanup();
370
+ }
371
+ }
372
+
373
+ export async function listAllTests(config = null) {
374
+ const discovery = await ensureDiscovery(config);
375
+ const tests = discovery.getAllTests();
376
+
377
+ if (isTestEnvironment()) {
378
+ return { success: true, tests };
379
+ }
380
+
381
+ discovery.listTests();
382
+ }
383
+
384
+ /**
385
+ * Discover tests and return them as an array
386
+ * @param {Object} config - Configuration object
387
+ * @returns {Array} Array of discovered tests
388
+ */
389
+ export async function discoverTests(config) {
390
+ const discovery = new TestDiscovery();
391
+ if (config?.execution?.testsDirectory) {
392
+ discovery.testsDirectory = resolve(process.cwd(), config.execution.testsDirectory);
393
+ }
394
+
395
+ try {
396
+ await discovery.discoverTests();
397
+ return Array.from(discovery.tests.values());
398
+ } catch (error) {
399
+ console.error('โŒ Error discovering tests:', error.message);
400
+ return [];
401
+ }
402
+ }