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/__init__.py +78 -0
- cursorflow/auto_updater.py +244 -0
- cursorflow/cli.py +408 -0
- cursorflow/core/agent.py +272 -0
- cursorflow/core/auth_handler.py +433 -0
- cursorflow/core/browser_controller.py +534 -0
- cursorflow/core/browser_engine.py +386 -0
- cursorflow/core/css_iterator.py +397 -0
- cursorflow/core/cursor_integration.py +744 -0
- cursorflow/core/cursorflow.py +649 -0
- cursorflow/core/error_correlator.py +322 -0
- cursorflow/core/event_correlator.py +182 -0
- cursorflow/core/file_change_monitor.py +548 -0
- cursorflow/core/log_collector.py +410 -0
- cursorflow/core/log_monitor.py +179 -0
- cursorflow/core/persistent_session.py +910 -0
- cursorflow/core/report_generator.py +282 -0
- cursorflow/log_sources/local_file.py +198 -0
- cursorflow/log_sources/ssh_remote.py +210 -0
- cursorflow/updater.py +512 -0
- cursorflow-1.2.0.dist-info/METADATA +444 -0
- cursorflow-1.2.0.dist-info/RECORD +25 -0
- cursorflow-1.2.0.dist-info/WHEEL +5 -0
- cursorflow-1.2.0.dist-info/entry_points.txt +2 -0
- cursorflow-1.2.0.dist-info/top_level.txt +1 -0
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()
|
cursorflow/core/agent.py
ADDED
@@ -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)
|