Skip to content

Commit 37f6d71

Browse files
committed
feat: integrate LLM configuration loading and auto-test startup in E2ETestAgentDemo
#532 - Added ConfigManager-based LLM setup with support for `~/.autodev/config.yaml` and environment variable fallback. - Enhanced auto-run behavior with LaunchedEffect to start tests on application launch. - Overhauled E2ETestAgent initialization with improved page loading and actionable element extraction. - Removed rule-based fallback logic for streamlined test execution. - Introduced `runE2ETestDemo` Gradle task for easier application execution.
1 parent f47d2d6 commit 37f6d71

File tree

2 files changed

+141
-107
lines changed

2 files changed

+141
-107
lines changed

mpp-viewer-web/build.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ compose.desktop {
150150
}
151151
}
152152

153+
// Task to run E2E Test Agent Demo
154+
tasks.register<JavaExec>("runE2ETestDemo") {
155+
group = "application"
156+
description = "Run the E2E Test Agent Demo application"
157+
mainClass.set("cc.unitmesh.viewer.web.e2etest.E2ETestAgentDemoKt")
158+
classpath = sourceSets["jvmMain"].runtimeClasspath
159+
160+
jvmArgs("--add-opens", "java.desktop/sun.awt=ALL-UNNAMED")
161+
jvmArgs("--add-opens", "java.desktop/java.awt.peer=ALL-UNNAMED")
162+
163+
if (System.getProperty("os.name").contains("Mac")) {
164+
jvmArgs("--add-opens", "java.desktop/sun.lwawt=ALL-UNNAMED")
165+
jvmArgs("--add-opens", "java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
166+
}
167+
}
168+
153169
// Add JVM flags for KCEF
154170
afterEvaluate {
155171
tasks.withType<JavaExec> {

mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/e2etest/E2ETestAgentDemo.kt

Lines changed: 125 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.*
55
import androidx.compose.foundation.lazy.LazyColumn
66
import androidx.compose.foundation.lazy.items
77
import androidx.compose.foundation.lazy.rememberLazyListState
8-
import androidx.compose.foundation.text.BasicTextField
98
import androidx.compose.material3.*
109
import androidx.compose.runtime.*
1110
import androidx.compose.ui.Alignment
@@ -19,10 +18,8 @@ import androidx.compose.ui.window.rememberWindowState
1918
import cc.unitmesh.agent.e2etest.E2ETestAgent
2019
import cc.unitmesh.agent.e2etest.E2ETestConfig
2120
import cc.unitmesh.agent.e2etest.E2ETestInput
22-
import cc.unitmesh.agent.e2etest.TestMemory
2321
import cc.unitmesh.agent.e2etest.executor.*
24-
import cc.unitmesh.agent.e2etest.model.*
25-
import cc.unitmesh.agent.e2etest.perception.PageStateExtractor
22+
import cc.unitmesh.config.ConfigManager
2623
import cc.unitmesh.llm.LLMService
2724
import cc.unitmesh.llm.ModelConfig
2825
import cc.unitmesh.llm.LLMProviderType
@@ -103,8 +100,8 @@ private fun E2ETestAgentDemoApp() {
103100
var logs by remember { mutableStateOf(listOf<LogEntry>()) }
104101
var testStatus by remember { mutableStateOf("Ready") }
105102
var isRunning by remember { mutableStateOf(false) }
106-
var targetUrl by remember { mutableStateOf("https://www.phodal.com") }
107-
var testGoal by remember { mutableStateOf("Navigate to the blog section and find an article about AI") }
103+
var targetUrl by remember { mutableStateOf("https://www.google.com") }
104+
var testGoal by remember { mutableStateOf("Search for 'Kotlin Multiplatform' and click on the first result") }
108105
var aiReasoning by remember { mutableStateOf("") }
109106
var currentStep by remember { mutableStateOf(0) }
110107
var totalSteps by remember { mutableStateOf(0) }
@@ -123,6 +120,50 @@ private fun E2ETestAgentDemoApp() {
123120
val browserDriver = remember { bridge.asBrowserDriver() }
124121
val pageStateExtractor = remember { bridge.asPageStateExtractor() }
125122

123+
// Auto-run test on startup
124+
LaunchedEffect(Unit) {
125+
println("[E2ETestDemo] LaunchedEffect started, waiting for KCEF...")
126+
// Wait for KCEF to initialize
127+
delay(3000)
128+
129+
println("[E2ETestDemo] Starting auto-test...")
130+
addLog("Auto-starting E2E test...", LogType.INFO)
131+
testStatus = "Starting..."
132+
133+
isRunning = true
134+
logs = emptyList()
135+
aiReasoning = ""
136+
currentStep = 0
137+
totalSteps = 0
138+
139+
try {
140+
runAIE2ETest(
141+
bridge = bridge,
142+
browserDriver = browserDriver,
143+
pageStateExtractor = pageStateExtractor,
144+
targetUrl = targetUrl,
145+
testGoal = testGoal,
146+
onLog = { msg, type ->
147+
println("[E2ETestDemo] $msg")
148+
addLog(msg, type)
149+
},
150+
onStatusChange = { testStatus = it },
151+
onReasoningUpdate = { aiReasoning = it },
152+
onStepUpdate = { step, total ->
153+
currentStep = step
154+
totalSteps = total
155+
}
156+
)
157+
} catch (e: Exception) {
158+
println("[E2ETestDemo] Error: ${e.message}")
159+
e.printStackTrace()
160+
}
161+
162+
isRunning = false
163+
addLog("Test completed!", LogType.SUCCESS)
164+
println("[E2ETestDemo] Test completed!")
165+
}
166+
126167
Row(modifier = Modifier.fillMaxSize()) {
127168
// Left panel: Controls and Logs
128169
Column(
@@ -323,43 +364,90 @@ private suspend fun runAIE2ETest(
323364
onStepUpdate: (Int, Int) -> Unit
324365
) {
325366
try {
326-
// Step 1: Check for LLM API key
327-
val apiKey = System.getenv("OPENAI_API_KEY")
328-
?: System.getenv("ANTHROPIC_API_KEY")
329-
?: System.getenv("DEEPSEEK_API_KEY")
330-
331-
if (apiKey.isNullOrBlank()) {
332-
onLog("No LLM API key found. Using rule-based fallback.", LogType.WARNING)
333-
onReasoningUpdate("No API key configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or DEEPSEEK_API_KEY environment variable.")
334-
runRuleBasedTest(bridge, browserDriver, pageStateExtractor, testGoal, onLog, onStatusChange, onStepUpdate)
335-
return
367+
// Step 1: Load LLM config from ConfigManager (~/.autodev/config.yaml)
368+
onLog("Loading LLM config from ~/.autodev/config.yaml...", LogType.INFO)
369+
370+
val configWrapper = ConfigManager.load()
371+
val activeConfig = configWrapper.getActiveModelConfig()
372+
373+
val modelConfig: ModelConfig? = if (activeConfig != null && configWrapper.isValid()) {
374+
onLog("Found config: ${configWrapper.getActiveName()} (${activeConfig.provider}/${activeConfig.modelName})", LogType.SUCCESS)
375+
activeConfig
376+
} else {
377+
// Fallback to environment variables
378+
onLog("No valid config in ~/.autodev/config.yaml, checking environment variables...", LogType.INFO)
379+
380+
val apiKey = System.getenv("OPENAI_API_KEY")
381+
?: System.getenv("ANTHROPIC_API_KEY")
382+
?: System.getenv("DEEPSEEK_API_KEY")
383+
384+
if (apiKey.isNullOrBlank()) {
385+
null
386+
} else {
387+
val provider = when {
388+
System.getenv("DEEPSEEK_API_KEY") != null -> LLMProviderType.DEEPSEEK
389+
System.getenv("ANTHROPIC_API_KEY") != null -> LLMProviderType.ANTHROPIC
390+
else -> LLMProviderType.OPENAI
391+
}
392+
393+
val modelName = when (provider) {
394+
LLMProviderType.DEEPSEEK -> "deepseek-chat"
395+
LLMProviderType.ANTHROPIC -> "claude-3-5-sonnet-20241022"
396+
else -> "gpt-4o-mini"
397+
}
398+
399+
onLog("Using environment variable: $provider / $modelName", LogType.INFO)
400+
401+
ModelConfig(
402+
provider = provider,
403+
modelName = modelName,
404+
apiKey = apiKey,
405+
temperature = 0.3,
406+
maxTokens = 2048
407+
)
408+
}
336409
}
337410

338-
val provider = when {
339-
System.getenv("DEEPSEEK_API_KEY") != null -> LLMProviderType.DEEPSEEK
340-
System.getenv("ANTHROPIC_API_KEY") != null -> LLMProviderType.ANTHROPIC
341-
else -> LLMProviderType.OPENAI
411+
if (modelConfig == null) {
412+
onLog("No LLM API key found. Please configure in ~/.autodev/config.yaml or set environment variable.", LogType.ERROR)
413+
onStatusChange("No API Key")
414+
onReasoningUpdate("No API key configured. Please configure in ~/.autodev/config.yaml or set OPENAI_API_KEY/ANTHROPIC_API_KEY/DEEPSEEK_API_KEY environment variable.")
415+
return
342416
}
343417

344-
val modelName = when (provider) {
345-
LLMProviderType.DEEPSEEK -> "deepseek-chat"
346-
LLMProviderType.ANTHROPIC -> "claude-3-5-sonnet-20241022"
347-
else -> "gpt-4o-mini"
418+
onLog("Using LLM: ${modelConfig.provider} / ${modelConfig.modelName}", LogType.INFO)
419+
420+
val llmService = LLMService.create(modelConfig)
421+
422+
// Step 2: Navigate to target URL first (required for bridge.isReady to become true)
423+
onLog("Navigating to $targetUrl...", LogType.INFO)
424+
onStatusChange("Navigating...")
425+
bridge.navigateTo(targetUrl)
426+
427+
// Wait for page load - bridge.isReady becomes true after page loads
428+
var waitCount = 0
429+
while (!bridge.isReady.value && waitCount < 100) {
430+
delay(100)
431+
waitCount++
348432
}
433+
delay(2000) // Extra time for rendering and JS bridge initialization
349434

350-
onLog("Using LLM: $provider / $modelName", LogType.INFO)
435+
if (!bridge.isReady.value) {
436+
onLog("Page failed to load within timeout", LogType.ERROR)
437+
onStatusChange("Page Load Failed")
438+
onReasoningUpdate("Failed to load page: $targetUrl")
439+
return
440+
}
351441

352-
val modelConfig = ModelConfig(
353-
provider = provider,
354-
modelName = modelName,
355-
apiKey = apiKey,
356-
temperature = 0.3,
357-
maxTokens = 2048
358-
)
442+
onLog("Page loaded successfully", LogType.SUCCESS)
359443

360-
val llmService = LLMService.create(modelConfig)
444+
// Step 3: Refresh accessibility tree and actionable elements
445+
onLog("Extracting page state...", LogType.INFO)
446+
bridge.refreshAccessibilityTree()
447+
bridge.refreshActionableElements()
448+
delay(500)
361449

362-
// Step 2: Create and initialize E2ETestAgent
450+
// Step 4: Create and initialize E2ETestAgent
363451
onLog("Initializing E2ETestAgent...", LogType.INFO)
364452
onStatusChange("Initializing Agent...")
365453

@@ -379,9 +467,9 @@ private suspend fun runAIE2ETest(
379467
agent.initializeWithDriver(browserDriver, pageStateExtractor)
380468

381469
if (!agent.isAvailable) {
382-
onLog("E2ETestAgent is not available. Using fallback.", LogType.WARNING)
383-
onReasoningUpdate("Agent initialization failed. Falling back to rule-based approach.")
384-
runRuleBasedTest(bridge, browserDriver, pageStateExtractor, testGoal, onLog, onStatusChange, onStepUpdate)
470+
onLog("E2ETestAgent is not available", LogType.ERROR)
471+
onStatusChange("Agent Not Available")
472+
onReasoningUpdate("Agent initialization failed. Check browser driver and page state extractor.")
385473
return
386474
}
387475

@@ -433,76 +521,6 @@ private suspend fun runAIE2ETest(
433521
}
434522
}
435523

436-
/**
437-
* Rule-based fallback when LLM is not available
438-
*/
439-
private suspend fun runRuleBasedTest(
440-
bridge: JvmWebEditBridge,
441-
browserDriver: BrowserDriver,
442-
pageStateExtractor: cc.unitmesh.agent.e2etest.perception.PageStateExtractor,
443-
testGoal: String,
444-
onLog: (String, LogType) -> Unit,
445-
onStatusChange: (String) -> Unit,
446-
onStepUpdate: (Int, Int) -> Unit
447-
) {
448-
onLog("Running rule-based test for: $testGoal", LogType.INFO)
449-
onStatusChange("Rule-based execution...")
450-
451-
val pageState = pageStateExtractor.extractPageState()
452-
val executor = JvmBrowserActionExecutor.withDriver(browserDriver)
453-
454-
// Simple heuristic: find elements matching keywords in the goal
455-
val keywords = testGoal.lowercase().split(" ").filter { it.length > 3 }
456-
457-
val matchingElements = pageState.actionableElements.filter { element ->
458-
val name = element.name?.lowercase() ?: ""
459-
keywords.any { keyword -> name.contains(keyword) }
460-
}
461-
462-
if (matchingElements.isEmpty()) {
463-
onLog("No elements matching goal keywords found", LogType.WARNING)
464-
onStatusChange("No matching elements")
465-
return
466-
}
467-
468-
onLog("Found ${matchingElements.size} elements matching goal", LogType.SUCCESS)
469-
onStepUpdate(0, matchingElements.size.coerceAtMost(3))
470-
471-
// Click up to 3 matching elements
472-
matchingElements.take(3).forEachIndexed { index, element ->
473-
onStepUpdate(index + 1, matchingElements.size.coerceAtMost(3))
474-
onLog("Clicking: [${element.tagId}] ${element.name}", LogType.INFO)
475-
476-
val context = ActionExecutionContext(
477-
tagMapping = pageState.actionableElements.associateBy { it.tagId }
478-
)
479-
executor.setContext(context)
480-
481-
val result = executor.click(element.tagId, ClickOptions())
482-
if (result.success) {
483-
onLog("Click succeeded", LogType.SUCCESS)
484-
} else {
485-
onLog("Click failed: ${result.error}", LogType.ERROR)
486-
}
487-
488-
delay(1000)
489-
}
490-
491-
onStatusChange("Rule-based test completed")
492-
}
493-
494-
private fun getTargetIdFromAction(action: TestAction): Int? {
495-
return when (action) {
496-
is TestAction.Click -> action.targetId
497-
is TestAction.Type -> action.targetId
498-
is TestAction.Hover -> action.targetId
499-
is TestAction.Assert -> action.targetId
500-
is TestAction.Select -> action.targetId
501-
is TestAction.Scroll -> action.targetId
502-
else -> null
503-
}
504-
}
505-
506524
@Composable
507525
private fun RestartView() {
508526
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

0 commit comments

Comments
 (0)