cursorflow 1.2.0__py3-none-any.whl

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.
cursorflow/cli.py ADDED
@@ -0,0 +1,408 @@
1
+ """
2
+ Command Line Interface for Cursor Testing Agent
3
+
4
+ Universal CLI that works with any web framework.
5
+ Provides simple commands for testing components across different architectures.
6
+ """
7
+
8
+ import click
9
+ import asyncio
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Dict
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+ from rich.progress import Progress, SpinnerColumn, TextColumn
17
+
18
+ from .core.agent import TestAgent
19
+
20
+ console = Console()
21
+
22
+ @click.group()
23
+ @click.version_option(version="1.0.0")
24
+ def main():
25
+ """Universal UI testing framework for any web technology"""
26
+ pass
27
+
28
+ @main.command()
29
+ @click.argument('test_name', required=False, default='ui-test')
30
+ @click.option('--base-url', '-u', default='http://localhost:3000',
31
+ help='Base URL for testing')
32
+ @click.option('--actions', '-a',
33
+ help='JSON file with test actions, or inline JSON string')
34
+ @click.option('--logs', '-l',
35
+ type=click.Choice(['local', 'ssh', 'docker', 'systemd']),
36
+ default='local',
37
+ help='Log source type')
38
+ @click.option('--config', '-c', type=click.Path(exists=True),
39
+ help='Configuration file path')
40
+ @click.option('--verbose', '-v', is_flag=True,
41
+ help='Verbose output')
42
+ def test(test_name, base_url, actions, logs, config, verbose):
43
+ """Test UI flows and interactions with real-time log monitoring"""
44
+
45
+ if verbose:
46
+ import logging
47
+ logging.basicConfig(level=logging.INFO)
48
+
49
+ # Parse actions
50
+ test_actions = []
51
+ if actions:
52
+ try:
53
+ # Check if it's a file path
54
+ if actions.endswith('.json') and Path(actions).exists():
55
+ with open(actions, 'r') as f:
56
+ test_actions = json.load(f)
57
+ console.print(f"📋 Loaded actions from [cyan]{actions}[/cyan]")
58
+ else:
59
+ # Try to parse as inline JSON
60
+ test_actions = json.loads(actions)
61
+ console.print(f"📋 Using inline actions")
62
+ except json.JSONDecodeError as e:
63
+ console.print(f"[red]❌ Invalid JSON in actions: {e}[/red]")
64
+ return
65
+ except Exception as e:
66
+ console.print(f"[red]❌ Failed to load actions: {e}[/red]")
67
+ return
68
+ else:
69
+ # Default actions - just navigate and screenshot
70
+ test_actions = [
71
+ {"navigate": "/"},
72
+ {"wait_for": "body"},
73
+ {"screenshot": "baseline"}
74
+ ]
75
+ console.print(f"📋 Using default actions (navigate + screenshot)")
76
+
77
+ # Load configuration
78
+ agent_config = {}
79
+ if config:
80
+ with open(config, 'r') as f:
81
+ agent_config = json.load(f)
82
+
83
+ console.print(f"🎯 Testing [bold]{test_name}[/bold] at [blue]{base_url}[/blue]")
84
+
85
+ # Initialize CursorFlow (framework-agnostic)
86
+ try:
87
+ from .core.cursorflow import CursorFlow
88
+ flow = CursorFlow(
89
+ base_url=base_url,
90
+ log_config={'source': logs, 'paths': ['logs/app.log']},
91
+ **agent_config
92
+ )
93
+ except Exception as e:
94
+ console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
95
+ return
96
+
97
+ # Execute test actions
98
+ try:
99
+ console.print(f"🚀 Executing {len(test_actions)} actions...")
100
+ results = asyncio.run(flow.execute_and_collect(test_actions))
101
+
102
+ console.print(f"✅ Test completed: {test_name}")
103
+ console.print(f"📊 Browser events: {len(results.get('browser_events', []))}")
104
+ console.print(f"📋 Server logs: {len(results.get('server_logs', []))}")
105
+ console.print(f"📸 Screenshots: {len(results.get('artifacts', {}).get('screenshots', []))}")
106
+
107
+ # Show correlations if found
108
+ timeline = results.get('organized_timeline', [])
109
+ if timeline:
110
+ console.print(f"⏰ Timeline events: {len(timeline)}")
111
+
112
+ # Save results to file for Cursor analysis
113
+ output_file = f"{test_name.replace(' ', '_')}_test_results.json"
114
+ with open(output_file, 'w') as f:
115
+ json.dump(results, f, indent=2, default=str)
116
+
117
+ console.print(f"💾 Full results saved to: [cyan]{output_file}[/cyan]")
118
+ console.print(f"📁 Artifacts stored in: [cyan].cursorflow/artifacts/[/cyan]")
119
+
120
+ except Exception as e:
121
+ console.print(f"[red]❌ Test failed: {e}[/red]")
122
+ if verbose:
123
+ import traceback
124
+ console.print(traceback.format_exc())
125
+ raise
126
+
127
+ @main.command()
128
+ @click.option('--project-path', '-p', default='.',
129
+ help='Project directory path')
130
+ @click.option('--environment', '-e',
131
+ type=click.Choice(['local', 'staging', 'production']),
132
+ default='local',
133
+ help='Target environment')
134
+ def auto_test(project_path, environment):
135
+ """Auto-detect framework and run appropriate tests"""
136
+
137
+ console.print("🔍 Auto-detecting project framework...")
138
+
139
+ framework = TestAgent.detect_framework(project_path)
140
+ console.print(f"Detected framework: [bold]{framework}[/bold]")
141
+
142
+ # Load project configuration
143
+ config_path = Path(project_path) / 'cursor-test-config.json'
144
+ if config_path.exists():
145
+ with open(config_path, 'r') as f:
146
+ project_config = json.load(f)
147
+ else:
148
+ console.print("[yellow]No cursor-test-config.json found, using defaults[/yellow]")
149
+ project_config = {}
150
+
151
+ # Get environment config
152
+ env_config = project_config.get('environments', {}).get(environment, {})
153
+ base_url = env_config.get('base_url', 'http://localhost:3000')
154
+
155
+ console.print(f"Testing [cyan]{environment}[/cyan] environment at [blue]{base_url}[/blue]")
156
+
157
+ # Auto-detect components and run smoke tests
158
+ asyncio.run(_run_auto_tests(framework, base_url, env_config))
159
+
160
+ async def _run_auto_tests(framework: str, base_url: str, config: Dict):
161
+ """Run automatic tests based on detected framework"""
162
+
163
+ try:
164
+ agent = TestAgent(framework, base_url, **config)
165
+
166
+ # Get available components
167
+ components = agent.adapter.get_available_components()
168
+
169
+ console.print(f"Found {len(components)} testable components")
170
+
171
+ # Run smoke tests for all components
172
+ results = await agent.run_smoke_tests(components)
173
+
174
+ # Display summary
175
+ display_smoke_test_summary(results)
176
+
177
+ except Exception as e:
178
+ console.print(f"[red]Auto-test failed: {e}[/red]")
179
+
180
+ @main.command()
181
+ @click.argument('project_path', default='.')
182
+ @click.option('--framework', '-f')
183
+ def install_rules(project_path, framework):
184
+ """Install CursorFlow rules and configuration in a project"""
185
+
186
+ console.print("🚀 Installing CursorFlow rules and configuration...")
187
+
188
+ try:
189
+ # Import and run the installation
190
+ from .install_cursorflow_rules import install_cursorflow_rules
191
+ success = install_cursorflow_rules(project_path)
192
+
193
+ if success:
194
+ console.print("[green]✅ CursorFlow rules installed successfully![/green]")
195
+ console.print("\nNext steps:")
196
+ console.print("1. Review cursorflow-config.json")
197
+ console.print("2. Install dependencies: pip install cursorflow && playwright install chromium")
198
+ console.print("3. Start testing: Use CursorFlow in Cursor!")
199
+ else:
200
+ console.print("[red]❌ Installation failed[/red]")
201
+
202
+ except Exception as e:
203
+ console.print(f"[red]Installation error: {e}[/red]")
204
+
205
+ @main.command()
206
+ @click.option('--force', is_flag=True, help='Force update even if no updates available')
207
+ @click.option('--project-dir', default='.', help='Project directory')
208
+ def update(force, project_dir):
209
+ """Update CursorFlow package and rules"""
210
+
211
+ console.print("🔄 Updating CursorFlow...")
212
+
213
+ try:
214
+ from .updater import update_cursorflow
215
+ import asyncio
216
+
217
+ success = asyncio.run(update_cursorflow(project_dir, force=force))
218
+
219
+ if success:
220
+ console.print("[green]✅ CursorFlow updated successfully![/green]")
221
+ else:
222
+ console.print("[red]❌ Update failed[/red]")
223
+
224
+ except Exception as e:
225
+ console.print(f"[red]Update error: {e}[/red]")
226
+
227
+ @main.command()
228
+ @click.option('--project-dir', default='.', help='Project directory')
229
+ def check_updates(project_dir):
230
+ """Check for available updates"""
231
+
232
+ try:
233
+ from .updater import check_updates
234
+ import asyncio
235
+
236
+ result = asyncio.run(check_updates(project_dir))
237
+
238
+ if "error" in result:
239
+ console.print(f"[red]Error checking updates: {result['error']}[/red]")
240
+ return
241
+
242
+ # Display update information
243
+ table = Table(title="CursorFlow Update Status")
244
+ table.add_column("Component", style="cyan")
245
+ table.add_column("Current", style="yellow")
246
+ table.add_column("Latest", style="green")
247
+ table.add_column("Status", style="bold")
248
+
249
+ # Package status
250
+ pkg_status = "🔄 Update Available" if result.get("version_update_available") else "✅ Current"
251
+ table.add_row(
252
+ "Package",
253
+ result.get("current_version", "unknown"),
254
+ result.get("latest_version", "unknown"),
255
+ pkg_status
256
+ )
257
+
258
+ # Rules status
259
+ rules_status = "🔄 Update Available" if result.get("rules_update_available") else "✅ Current"
260
+ table.add_row(
261
+ "Rules",
262
+ result.get("current_rules_version", "unknown"),
263
+ result.get("latest_rules_version", "unknown"),
264
+ rules_status
265
+ )
266
+
267
+ # Dependencies status
268
+ deps_status = "✅ Current" if result.get("dependencies_current") else "⚠️ Needs Update"
269
+ table.add_row("Dependencies", "-", "-", deps_status)
270
+
271
+ console.print(table)
272
+
273
+ # Show update commands if needed
274
+ if result.get("version_update_available") or result.get("rules_update_available"):
275
+ console.print("\n💡 Run [bold]cursorflow update[/bold] to install updates")
276
+
277
+ except Exception as e:
278
+ console.print(f"[red]Error: {e}[/red]")
279
+
280
+ @main.command()
281
+ @click.option('--project-dir', default='.', help='Project directory')
282
+ def install_deps(project_dir):
283
+ """Install or update CursorFlow dependencies"""
284
+
285
+ console.print("🔧 Installing CursorFlow dependencies...")
286
+
287
+ try:
288
+ from .updater import install_dependencies
289
+ import asyncio
290
+
291
+ success = asyncio.run(install_dependencies(project_dir))
292
+
293
+ if success:
294
+ console.print("[green]✅ Dependencies installed successfully![/green]")
295
+ else:
296
+ console.print("[red]❌ Dependency installation failed[/red]")
297
+
298
+ except Exception as e:
299
+ console.print(f"[red]Error: {e}[/red]")
300
+
301
+ @main.command()
302
+ @click.argument('project_path')
303
+ # Framework detection removed - CursorFlow is framework-agnostic
304
+ def init(project_path):
305
+ """Initialize cursor testing for a project"""
306
+
307
+ project_dir = Path(project_path)
308
+
309
+ # Create configuration file (framework-agnostic)
310
+ config_template = {
311
+ 'environments': {
312
+ 'local': {
313
+ 'base_url': 'http://localhost:3000',
314
+ 'logs': 'local',
315
+ 'log_paths': {
316
+ 'app': 'logs/app.log'
317
+ }
318
+ },
319
+ 'staging': {
320
+ 'base_url': 'https://staging.example.com',
321
+ 'logs': 'ssh',
322
+ 'ssh_config': {
323
+ 'hostname': 'staging-server',
324
+ 'username': 'deploy'
325
+ },
326
+ 'log_paths': {
327
+ 'app_error': '/var/log/app/error.log'
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ # Universal configuration works for any web application
334
+
335
+ # Save configuration
336
+ config_path = project_dir / 'cursor-test-config.json'
337
+ with open(config_path, 'w') as f:
338
+ json.dump(config_template, f, indent=2)
339
+
340
+ console.print(f"[green]Initialized cursor testing for project[/green]")
341
+ console.print(f"Configuration saved to: {config_path}")
342
+ console.print("\nNext steps:")
343
+ console.print("1. Edit cursor-test-config.json with your specific settings")
344
+ console.print("2. Run: cursor-test auto-test")
345
+
346
+ def display_test_results(results: Dict):
347
+ """Display test results in rich format"""
348
+
349
+ # Summary table
350
+ table = Table(title="Test Results Summary")
351
+ table.add_column("Component", style="cyan")
352
+ table.add_column("Framework", style="magenta")
353
+ table.add_column("Success", style="green")
354
+ table.add_column("Errors", style="red")
355
+ table.add_column("Warnings", style="yellow")
356
+
357
+ summary = results.get('correlations', {}).get('summary', {})
358
+
359
+ table.add_row(
360
+ results.get('component', 'unknown'),
361
+ results.get('framework', 'unknown'),
362
+ "✅" if results.get('success', False) else "❌",
363
+ str(summary.get('error_count', 0)),
364
+ str(summary.get('warning_count', 0))
365
+ )
366
+
367
+ console.print(table)
368
+
369
+ # Critical issues
370
+ critical_issues = results.get('correlations', {}).get('critical_issues', [])
371
+ if critical_issues:
372
+ console.print(f"\n[red bold]🚨 {len(critical_issues)} Critical Issues Found:[/red bold]")
373
+ for i, issue in enumerate(critical_issues[:3], 1):
374
+ browser_event = issue['browser_event']
375
+ server_logs = issue['server_logs']
376
+ console.print(f" {i}. {browser_event.get('action', 'Unknown action')} → {len(server_logs)} server errors")
377
+
378
+ # Recommendations
379
+ recommendations = results.get('correlations', {}).get('recommendations', [])
380
+ if recommendations:
381
+ console.print(f"\n[blue bold]💡 Recommendations:[/blue bold]")
382
+ for rec in recommendations[:3]:
383
+ console.print(f" • {rec.get('title', 'Unknown recommendation')}")
384
+
385
+ def display_smoke_test_summary(results: Dict):
386
+ """Display smoke test results for multiple components"""
387
+
388
+ table = Table(title="Smoke Test Results")
389
+ table.add_column("Component", style="cyan")
390
+ table.add_column("Status", style="bold")
391
+ table.add_column("Errors", style="red")
392
+ table.add_column("Duration", style="blue")
393
+
394
+ for component_name, result in results.items():
395
+ if result.get('success', False):
396
+ status = "[green]✅ PASS[/green]"
397
+ else:
398
+ status = "[red]❌ FAIL[/red]"
399
+
400
+ error_count = len(result.get('correlations', {}).get('critical_issues', []))
401
+ duration = f"{result.get('duration', 0):.1f}s"
402
+
403
+ table.add_row(component_name, status, str(error_count), duration)
404
+
405
+ console.print(table)
406
+
407
+ if __name__ == '__main__':
408
+ main()
@@ -0,0 +1,272 @@
1
+ """
2
+ Universal Test Agent - Main orchestrator class
3
+
4
+ This is the primary interface for the universal testing framework.
5
+ It coordinates browser automation, log monitoring, and report generation
6
+ for any web architecture.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Dict, List, Optional, Any
12
+ from pathlib import Path
13
+
14
+ from .browser_engine import BrowserEngine
15
+ from .log_monitor import LogMonitor
16
+ from .error_correlator import ErrorCorrelator
17
+ from .report_generator import ReportGenerator
18
+
19
+ class TestAgent:
20
+ """Universal testing agent that adapts to any web framework"""
21
+
22
+ def __init__(
23
+ self,
24
+ framework: str,
25
+ base_url: str,
26
+ logs: str = 'local',
27
+ **config
28
+ ):
29
+ """
30
+ Initialize universal test agent
31
+
32
+ Args:
33
+ framework: Target framework ('mod_perl', 'react', 'php', 'django', 'vue')
34
+ base_url: Base URL for testing (e.g., 'http://localhost:3000')
35
+ logs: Log source type ('local', 'ssh', 'docker', 'systemd', 'cloud')
36
+ **config: Framework and environment specific configuration
37
+ """
38
+ self.framework = framework
39
+ self.base_url = base_url
40
+ self.config = config
41
+
42
+ # Load framework-specific adapter
43
+ self.adapter = self._load_adapter(framework)
44
+
45
+ # Initialize core components
46
+ self.browser_engine = BrowserEngine(base_url, self.adapter)
47
+ self.log_monitor = LogMonitor(logs, config.get('log_config', {}))
48
+ self.error_correlator = ErrorCorrelator(self.adapter.get_error_patterns())
49
+ self.report_generator = ReportGenerator()
50
+
51
+ # State tracking
52
+ self.current_test = None
53
+ self.test_results = []
54
+
55
+ # Setup logging
56
+ logging.basicConfig(level=logging.INFO)
57
+ self.logger = logging.getLogger(__name__)
58
+
59
+ def _load_adapter(self, framework: str):
60
+ """Dynamically load framework-specific adapter"""
61
+ adapter_map = {
62
+ 'mod_perl': 'ModPerlAdapter',
63
+ 'react': 'ReactAdapter',
64
+ 'vue': 'VueAdapter',
65
+ 'php': 'PHPAdapter',
66
+ 'django': 'DjangoAdapter',
67
+ 'flask': 'FlaskAdapter'
68
+ }
69
+
70
+ if framework not in adapter_map:
71
+ raise ValueError(f"Unsupported framework: {framework}")
72
+
73
+ # Dynamic import of adapter
74
+ module_name = f"..adapters.{framework}"
75
+ class_name = adapter_map[framework]
76
+
77
+ try:
78
+ module = __import__(module_name, fromlist=[class_name], level=1)
79
+ adapter_class = getattr(module, class_name)
80
+ return adapter_class()
81
+ except ImportError:
82
+ raise ImportError(f"Adapter for {framework} not found")
83
+
84
+ @classmethod
85
+ def auto_configure(cls, project_path: str, environment: str = 'local'):
86
+ """
87
+ Automatically configure agent based on project structure
88
+
89
+ Args:
90
+ project_path: Path to project directory
91
+ environment: Target environment ('local', 'staging', 'production')
92
+ """
93
+ framework = cls._detect_framework(project_path)
94
+ config = cls._load_project_config(project_path, environment)
95
+
96
+ return cls(framework, config['base_url'], config['logs'], **config)
97
+
98
+ @staticmethod
99
+ def _detect_framework(project_path: str) -> str:
100
+ """Detect framework from project structure"""
101
+ path = Path(project_path)
102
+
103
+ # Framework detection patterns
104
+ patterns = {
105
+ 'react': ['package.json', 'src/App.jsx', 'next.config.js'],
106
+ 'mod_perl': ['*.comp', '*.smpl', '~openSAS/'],
107
+ 'php': ['composer.json', 'artisan', 'app/Http/'],
108
+ 'django': ['manage.py', 'settings.py', 'wsgi.py'],
109
+ 'vue': ['vue.config.js', 'src/main.js', 'nuxt.config.js']
110
+ }
111
+
112
+ for framework, indicators in patterns.items():
113
+ if cls._check_patterns(path, indicators):
114
+ return framework
115
+
116
+ return 'generic'
117
+
118
+ async def test(
119
+ self,
120
+ component_name: str,
121
+ test_params: Optional[Dict] = None,
122
+ workflows: Optional[List[str]] = None
123
+ ) -> Dict[str, Any]:
124
+ """
125
+ Test a specific component with optional workflows
126
+
127
+ Args:
128
+ component_name: Name of component to test
129
+ test_params: Parameters for the test (e.g., {'orderid': '123'})
130
+ workflows: List of workflows to execute (e.g., ['load', 'interact'])
131
+
132
+ Returns:
133
+ Test results with browser events, server logs, and correlations
134
+ """
135
+ self.logger.info(f"Starting test for {component_name} with {self.framework}")
136
+
137
+ # Load test definition
138
+ test_definition = self.adapter.get_test_definition(component_name)
139
+
140
+ # Start log monitoring
141
+ await self.log_monitor.start_monitoring()
142
+
143
+ # Initialize browser
144
+ await self.browser_engine.initialize()
145
+
146
+ try:
147
+ # Execute test workflows
148
+ test_results = await self._execute_test_workflows(
149
+ test_definition, test_params, workflows
150
+ )
151
+
152
+ # Stop monitoring and get logs
153
+ server_logs = await self.log_monitor.stop_monitoring()
154
+
155
+ # Correlate browser events with server logs
156
+ correlations = self.error_correlator.correlate_events(
157
+ test_results['browser_events'],
158
+ server_logs
159
+ )
160
+
161
+ # Generate comprehensive results
162
+ results = {
163
+ 'framework': self.framework,
164
+ 'component': component_name,
165
+ 'test_params': test_params,
166
+ 'browser_results': test_results,
167
+ 'server_logs': server_logs,
168
+ 'correlations': correlations,
169
+ 'success': len(correlations.get('errors', [])) == 0,
170
+ 'timestamp': asyncio.get_event_loop().time()
171
+ }
172
+
173
+ self.test_results.append(results)
174
+ return results
175
+
176
+ finally:
177
+ await self.browser_engine.cleanup()
178
+
179
+ async def _execute_test_workflows(
180
+ self,
181
+ test_definition: Dict,
182
+ test_params: Optional[Dict],
183
+ workflows: Optional[List[str]]
184
+ ) -> Dict:
185
+ """Execute specified test workflows"""
186
+
187
+ # Build test URL
188
+ test_url = self.adapter.build_url(self.base_url, test_definition, test_params)
189
+
190
+ # Navigate to component
191
+ await self.browser_engine.navigate(test_url)
192
+
193
+ # Execute workflows
194
+ workflow_results = {}
195
+ workflows = workflows or ['smoke_test']
196
+
197
+ for workflow_name in workflows:
198
+ if workflow_name in test_definition.get('workflows', {}):
199
+ workflow_def = test_definition['workflows'][workflow_name]
200
+ result = await self.browser_engine.execute_workflow(workflow_def)
201
+ workflow_results[workflow_name] = result
202
+ else:
203
+ self.logger.warning(f"Workflow {workflow_name} not found in test definition")
204
+
205
+ return {
206
+ 'url': test_url,
207
+ 'workflows': workflow_results,
208
+ 'browser_events': self.browser_engine.get_events(),
209
+ 'performance_metrics': await self.browser_engine.get_performance_metrics(),
210
+ 'console_errors': await self.browser_engine.get_console_errors(),
211
+ 'network_requests': self.browser_engine.get_network_requests()
212
+ }
213
+
214
+ def generate_report(self, results: Optional[Dict] = None) -> str:
215
+ """Generate Cursor-friendly test report"""
216
+
217
+ if results is None:
218
+ results = self.test_results[-1] if self.test_results else {}
219
+
220
+ return self.report_generator.create_markdown_report(results)
221
+
222
+ def open_results_in_cursor(self, results: Optional[Dict] = None):
223
+ """Open test results in Cursor IDE"""
224
+
225
+ report_path = self.report_generator.save_report(results or self.test_results[-1])
226
+
227
+ # Open in Cursor
228
+ import subprocess
229
+ subprocess.run(['cursor', report_path])
230
+
231
+ # Also open any files with errors
232
+ if results and 'correlations' in results:
233
+ for error in results['correlations'].get('errors', []):
234
+ if 'file_path' in error and 'line_number' in error:
235
+ file_with_line = f"{error['file_path']}:{error['line_number']}"
236
+ subprocess.run(['cursor', file_with_line])
237
+
238
+ async def run_smoke_tests(self, components: Optional[List[str]] = None) -> Dict:
239
+ """Run smoke tests for specified components or all available"""
240
+
241
+ if components is None:
242
+ components = self.adapter.get_available_components()
243
+
244
+ results = {}
245
+ for component in components:
246
+ try:
247
+ result = await self.test(component, workflows=['smoke_test'])
248
+ results[component] = result
249
+ except Exception as e:
250
+ results[component] = {'error': str(e), 'success': False}
251
+
252
+ return results
253
+
254
+ async def continuous_monitoring(self, component_name: str, interval: int = 60):
255
+ """Continuously monitor component health"""
256
+
257
+ while True:
258
+ try:
259
+ result = await self.test(component_name, workflows=['health_check'])
260
+
261
+ if not result['success']:
262
+ # Alert on failures
263
+ await self._send_alert(component_name, result)
264
+
265
+ await asyncio.sleep(interval)
266
+
267
+ except KeyboardInterrupt:
268
+ self.logger.info("Continuous monitoring stopped")
269
+ break
270
+ except Exception as e:
271
+ self.logger.error(f"Monitoring error: {e}")
272
+ await asyncio.sleep(interval)