AI-powered mobile UI test automation for Android, iOS, and React Native. A Kotlin/Ktor backend exposes an HTTP API that hands natural-language scenarios to a Koog-powered LLM agent, which drives a real device or emulator. A React/Vite dashboard under web/ lets users author scenarios and trigger runs.
Full documentation lives in docs/. Start there for anything non-trivial.
| Doc | What it covers |
|---|---|
| docs/architecture.md | Runtime topology, request lifecycle, module map |
| docs/getting-started.md | Prerequisites, env, first run |
| docs/api.md | POST /run-test, POST /stop-test, POST /config payloads + errors |
| docs/ai-agent.md | MobileTestAgent, strategy graph, system prompt, executors |
| docs/tools.md | Tool catalog, status-prefix convention, utility layer |
| docs/frontend.md | React/Vite dashboard internals |
| docs/dependencies.md | Every third-party library, by reason |
src/main/kotlin/
├── server/ # Ktor: Application, Routing, HTTP, Monitoring
│ └── model/ # AgentRequest, MobileTesterConfigAPI (wire DTOs)
└── agent/
├── MobileTestAgent.kt # Singleton — builds & runs the Koog AIAgent
├── strategy/ # TestingStrategy.kt — Koog graph
├── executor/ # Per-provider subfolders (anthropic, deepSeek, google, ollama, openRouter), each holding ExecutorInfo impls
├── model/ # MobileTesterConfig, TestScenarioReport
└── tool/
├── mobile/test/ # MobileTestTools, ReportingTools, utils/
└── reporting/ # ReportingTools scaffolding (not yet wired)
web/ # React 19 + Vite dashboard
docs/ # Source of truth for design + API docs
- JDK 21 (Amazon Corretto), Kotlin 2.3.0, Gradle wrapper (8.11+)
- Ktor 3.1.3 (Netty) +
kotlinx.serialization - Koog Agents 1.0.0 —
AIAgent, strategy DSL,@Toolreflection - dotenv-kotlin — loads
.envfrom project root
- React 19 + react-router-dom 7 on Vite 7 + TypeScript 5.8
- axios for
POST /run-testandPOST /stop-test, nativefetchfor/config - Firebase Firestore for scenario persistence
- mermaid for architecture diagram on About page
- Vite dev server proxies
/api/*→http://localhost:8080
All implement ExecutorInfo in agent/executor/<provider>/. Selected via POST /config (executorInfoId string, case-sensitive).
executorInfoId |
Class | Env var |
|---|---|---|
DeepSeekV4Flash (default) |
DeepSeekV4FlashExecutor |
DEEP_SEEK_KEY |
Gemini3Pro |
Gemini3ProExecutor (Gemini 3 Pro Preview) |
GEMINI_API_KEY |
Opus47 |
Opus47Executor (Claude Opus 4.7) |
CLAUDE_API_KEY |
GPT52Pro |
GPT52ProExecutor (OpenRouter GPT-5.2 Pro) |
OPEN_ROUTER |
QWEN36B |
QWEN36BExecutor (local Ollama, Qwen 3 0.6B) |
— |
Llama4 |
Llama4Executor (local Ollama) |
— |
Grok8BExecutor |
Grok8BExecutor (local Ollama) |
— |
These behaviors are load-bearing — see docs/ai-agent.md and docs/tools.md for the full reasoning.
- Status-prefixed tool returns. Every
@Toolreturns aStringstarting with one ofOK | TAPPED | VISIBLE | NOT_VISIBLE | NOT_FOUND | AMBIGUOUS | ERROR | TIMEOUT. The system prompt tells the LLM to pattern-match the prefix. Preserve this when adding tools — return strings, never throw. - Device serial pinning.
AdbUtils.runAdb()injects-s <serial>afterconnectDevice()picks a target. Don't bypass it with rawProcessBuilder("adb", …)calls. MAX_TOKENS_THRESHOLD = 8000inTestingStrategy.kt. Lower values trigger compression too aggressively and the agent forgets which step it's on.startTestingScenarioonce, first. The system prompt forbids any "tap launcher to open the app" recovery — launch is handled programmatically. The agent stops immediately after the final-step summary; there is no explicit close step.hideKeyboardusesKEYCODE_BACK(4), notKEYCODE_ESCAPE. Empirically required on the target device; documented inline.
./start.sh # Start backend (:8080) + web (:5173) and open browser
./gradlew run # Backend only
./gradlew compileKotlin # Type-check Kotlin
adb devices # Confirm device/emulator visibility
cd web && npm install # Frontend deps
cd web && npm run dev # Vite dev server on :5173 (proxies /api → :8080)
cd web && npm run build # tsc -b && vite build
cd web && npm run lint # ESLint.env at project root (loaded by dotenv-kotlin, gitignored). See .env.example for the template. At minimum one LLM key plus HOME_PATH (where screenshots/recordings are pulled to). For the dashboard, also VITE_FIREBASE_* keys.
- New
@Tool— add a method toMobileTestToolswith@Tool+@LLMDescription. Koog reflection picks it up; no registry edits needed. Return a status-prefixed string. - New executor — implement
ExecutorInfoinagent/executor/, then add awhen-branch inMobileTesterConfigAPI.toMobileConfig()and (optionally) an<option>inweb/src/pages/settings/Settings.tsx.
- Sample Android app under test: mobile-tester-agent-sample-app
- Background reading: Building an Agentic AI Mobile Tester with Koog and Kotlin