package cc.unitmesh.server.cli import cc.unitmesh.e2e.dsl.E2EDslGenerator import cc.unitmesh.e2e.dsl.E2EDslIntegration import cc.unitmesh.llm.LLMProviderType import cc.unitmesh.llm.LLMService import cc.unitmesh.llm.ModelConfig import com.charleskorn.kaml.Yaml import kotlinx.coroutines.runBlocking import java.io.File /** * E2E Test CLI - AI-driven E2E test scenario generator * * This CLI generates E2E test scenarios in DSL format from natural language descriptions. * The generated DSL can be executed with Playwright or other browser automation tools. * * Usage: * ```bash * ./gradlew :mpp-ui:runE2ECli -Pe2eUrl="https://example.com" -Pe2eGoal="Login with valid credentials" * ``` * * @see Issue #532 */ object E2ECli { @JvmStatic fun main(args: Array) { println("=".repeat(80)) println("AutoDev E2E Test Agent CLI") println("=".repeat(80)) // Parse arguments val targetUrl = System.getProperty("e2eUrl") ?: args.getOrNull(0) ?: run { System.err.println("Usage: -Pe2eUrl= -Pe2eGoal= [-Pe2eOutput=]") System.err.println("Example: -Pe2eUrl=\"https://example.com\" -Pe2eGoal=\"Login with valid credentials\"") return } val testGoal = System.getProperty("e2eGoal") ?: args.getOrNull(1) ?: run { System.err.println("Usage: -Pe2eUrl= -Pe2eGoal= [-Pe2eOutput=]") return } val outputFile = System.getProperty("e2eOutput") ?: args.getOrNull(2) println("🌐 Target URL: $targetUrl") println("🎯 Test Goal: $testGoal") if (outputFile != null) { println("📄 Output File: $outputFile") } println() runBlocking { try { val startTime = System.currentTimeMillis() // Load LLM configuration val modelConfig = loadLLMConfig() ?: run { System.err.println("❌ No LLM configuration found") System.err.println(" Please configure in ~/.autodev/config.yaml or set environment variables") return@runBlocking } println("🤖 Using LLM: ${modelConfig.provider} / ${modelConfig.modelName}") println() val llmService = LLMService.create(modelConfig) val dslIntegration = E2EDslIntegration(llmService) val dslGenerator = E2EDslGenerator() println("🧠 Generating E2E test scenario from description...") println() // Generate scenario directly from description - no page state needed val scenario = dslIntegration.generateScenarioFromDescription( description = testGoal, startUrl = targetUrl ) val totalTime = System.currentTimeMillis() - startTime if (scenario != null) { println("✅ Scenario generated successfully!") println() println("=".repeat(80)) println("📋 Generated E2E DSL:") println("=".repeat(80)) println() val dsl = dslGenerator.generate(scenario) println(dsl) // Save to file if specified if (outputFile != null) { File(outputFile).writeText(dsl) println() println("💾 Saved to: $outputFile") } println() println("=".repeat(80)) println("📊 Summary:") println(" Scenario: ${scenario.name}") println(" Steps: ${scenario.steps.size}") println(" Time: ${totalTime}ms") } else { println("❌ Failed to generate scenario") } } catch (e: Exception) { System.err.println("❌ Error: ${e.message}") e.printStackTrace() } } } private fun loadLLMConfig(): ModelConfig? { // Try config file first val configFile = File(System.getProperty("user.home"), ".autodev/config.yaml") if (configFile.exists()) { try { val yamlContent = configFile.readText() val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false)) val config = yaml.decodeFromString(AutoDevConfig.serializer(), yamlContent) val activeName = config.active val activeConfig = config.configs.find { it.name == activeName } if (activeConfig != null) { val providerType = when (activeConfig.provider.lowercase()) { "openai" -> LLMProviderType.OPENAI "anthropic" -> LLMProviderType.ANTHROPIC "google" -> LLMProviderType.GOOGLE "deepseek" -> LLMProviderType.DEEPSEEK "ollama" -> LLMProviderType.OLLAMA "openrouter" -> LLMProviderType.OPENROUTER "glm" -> LLMProviderType.GLM "qwen" -> LLMProviderType.QWEN "kimi" -> LLMProviderType.KIMI else -> LLMProviderType.CUSTOM_OPENAI_BASE } return ModelConfig( provider = providerType, modelName = activeConfig.model, apiKey = activeConfig.apiKey, temperature = activeConfig.temperature ?: 0.3, maxTokens = activeConfig.maxTokens ?: 4096, baseUrl = activeConfig.baseUrl ?: "" ) } } catch (e: Exception) { println("⚠️ Failed to load config file: ${e.message}") } } // Fallback to environment variables val apiKey = System.getenv("OPENAI_API_KEY") ?: System.getenv("ANTHROPIC_API_KEY") ?: System.getenv("DEEPSEEK_API_KEY") if (apiKey.isNullOrBlank()) return null val provider = when { System.getenv("DEEPSEEK_API_KEY") != null -> LLMProviderType.DEEPSEEK System.getenv("ANTHROPIC_API_KEY") != null -> LLMProviderType.ANTHROPIC else -> LLMProviderType.OPENAI } val modelName = when (provider) { LLMProviderType.DEEPSEEK -> "deepseek-chat" LLMProviderType.ANTHROPIC -> "claude-3-5-sonnet-20241022" else -> "gpt-4o-mini" } return ModelConfig( provider = provider, modelName = modelName, apiKey = apiKey, temperature = 0.3, maxTokens = 4096 ) } }