Inspiration
"The UI tests are red again."
I've sat in sprint retrospectives where someone says that and everyone nods like it's weather. Not a problem to fix — a season to endure.
I spent ten years writing Android UI tests that broke every sprint. A designer moves a button, half the suite goes red, a QA engineer loses a day to selectors. What eventually struck me wasn't the breakage. It was that nobody found it strange. The decision-makers above me were experienced, respected people who had built careers shipping great mobile products — and they had accepted these tools as the ceiling. The suites stayed brittle, the teams stayed frustrated, and leadership stayed confident we were using the best tools available.
That isn't incompetence. It's inertia. When Google's own tools fail the same way, and the entire industry agrees they're best practice, questioning them feels like questioning gravity.
So nobody questioned the premise underneath it: that a human has to write every step in advance. XPath selectors, YAML flows, page objects — the tools got friendlier over the years, but the work never changed. Someone who already understood the app had to anticipate every path, which means you only ever check the paths you already suspected. A green suite doesn't mean the app works. It means the app still does the things one person thought to write down, some sprints ago.
The same thing happens one floor up, with different vocabulary. Someone senior says a screen has a problem, and by the next day it's true — not because anyone checked, but because of who said it. A hunch becomes a PRD, the PRD kicks off a months-long measurement chain, and the chain ends in a number everyone trusts and nobody validated. You ship the winner. You verified none of it.
The people were never the problem. Every PM and analyst I worked with was sharp and trying hard. The problem was that going to look was expensive — checking the screen meant a human, a device, and an afternoon; checking the number meant re-deriving someone else's pipeline. Expensive enough that "someone said so" won by default.
Drengr is what I built to make checking cheap.
What it does
Drengr is two products sharing one question: what is this app actually doing?
Actuator gives an AI agent eyes and hands on a real device. It captures the screen,
parses the UI tree, and executes taps, types, swipes and gestures over ADB (Android),
simctl (iOS), or Appium (cloud device farms). You describe a goal in plain English —
log in with the test account and complete checkout — and the agent decides each step
itself. There is no script. It runs as an MCP server, so Claude or any MCP client drives
the phone directly, and as a standalone CLI for CI.
Analytics is an SDK that ships inside your app and reports what real users actually experience. Raw PII is sealed on the device before anything leaves it — it is never readable on our servers. Every number it renders carries an honest bound: what was counted exactly, what was estimated, and how wide the interval is. When it doesn't know, it says so instead of drawing a confident line through four data points.
Actuator answers the question by going and looking. Analytics answers it by measuring honestly enough that the answer survives being questioned.
Where it stands today
I would rather state this plainly than let a demo imply more than is true.
| Status | What that means | |
|---|---|---|
| Actuator | Early-stage stable | Shipped and published on npm, currently v0.10.2. The tool surface has settled, 563 tests cover the core, and I use it daily. Early in adoption, not in reliability. |
| Analytics | Work in progress | Capture SDKs are live across all four platforms and the ingest and query pipeline runs in production. The auto-understanding layer is real but actively being built and hardened. Treat it as under construction. |
How I built it
The core is a single Rust binary (drengr-mcp) exposing three MCP tools —
drengr_look, drengr_do, drengr_query — over JSON-RPC on stdio.
The agent loop is OODA: Observe (screenshot + UI tree) → Orient (a compact text scene plus a diff-based situation report) → Decide (LLM) → Act. The interesting part is Orient. Sending a screenshot to a vision model on every step costs around 100KB and real money, so the default path renders the screen as a text description of roughly 300 tokens and escalates to vision only when text can't disambiguate:
$$ \text{escalate} \iff \frac{|{e \in E : \mathrm{labeled}(e)}|}{|E|} < 0.6 $$
If more than 40% of the interactive elements on screen carry no resource-id, text, or content-description, text alone won't let the model pick the right one, so we pay for vision. Otherwise we don't.
Around that loop:
- A persisted world model. An exploration run builds a breadth-first navigation graph of the app and caches it, so a later run resolves "go to settings" into a next hop with no LLM call at all. It carries a 7-day expiry, because app UIs change under you and a stale map is worse than no map.
- Network capture through an in-process MITM proxy, so a failed tap can be explained by the 502 that caused it.
- Four capture SDKs at parity — Kotlin, Swift, TypeScript and Dart.
- Two data planes — ClickHouse for analytics events, Postgres for control, with the query layer running as Deno edge functions.
- Provider-agnostic inference. Gemini, OpenAI, Anthropic, Groq, Together, Fireworks, or a local Ollama model, which needs no key at all.
Challenges I ran into
MCP servers must not print. The transport is JSON-RPC over stdio, so one stray
println! from any dependency corrupts the stream and the client disconnects with no
useful error. Every stdout write in the process had to be redirected to stderr. I lost an
evening to a progress bar.
Semantic locators beat coordinates — until there's nothing to be semantic about. Matching on resource-id and text fixes the "breaks across screen sizes" problem that makes XPath suites so brittle. But Flutter apps, webviews and canvas games expose no accessibility tree at all, so the fallback is vision plus normalized 0–1 coordinates rather than pixels. While writing this submission I found that the escalation rule above treated a screen with zero elements as "100% labeled," which meant headless runs on exactly those apps didn't escalate to vision when they most needed to. Found it, fixed it, wrote the test — and I'd rather say that than pretend I'd caught it earlier.
Determinism is a real cost, not a rough edge. Run an instrumented test twice and you get the same result. Run Drengr twice against the same goal and you may get different paths, different findings, different coverage. That's an advantage for exploring what nobody scripted and a genuine limitation for regression testing. I present it as a trade, not as something a later version will quietly fix.
Honesty is harder than accuracy. The dangerous bug isn't a wrong number — it's a true number inside a false sentence. One analytics view correctly excluded users too new to have a full observation window, then told a brand-new customer "no installs observed yet" while they were actively sending us hundreds. The math was right and the screen was lying. Splitting that into three distinct states — still warming, genuinely empty, we couldn't look — took longer than the measurement itself.
Accomplishments I'm proud of
Four SDKs that behave identically. A single static binary with no runtime dependencies that installs in one command. Raw PII that is sealed on-device, so the honest answer to "can you read my users' data?" is no, by construction rather than by policy. And a product that tells you what it doesn't know.
What I learned
Correctness lives in boring places. A pagination cursor using < instead of <=
silently dropped rows whenever two events shared a timestamp — invisible to every test,
wrong on real data. I no longer trust a feature because it compiles or because the suite
is green. I trust it once I've run it against real data and checked the output by hand.
There are 563 tests in the Rust core, and not one of them would have caught that.
Which, uncomfortably, is the same lesson as the retrospective I started with. A green suite was never proof. I just happened to be the one nodding.
What's next for Drengr
Hardening Analytics toward the same bar Actuator already holds. Closing the treeless-app gap above. Deeper CI integration so an agent-driven run can gate a merge. The long arc is intelligence out, not dashboards in — understand, then foresee, then act.
Built With
- adb
- appium
- clickhouse
- dart
- deno
- flutter
- kotlin
- mcp
- nextjs
- npm
- postgresql
- react
- rust
- shell
- supabase
- swift
- tokio
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.