interestingTags hardcoded selectors from a specific app (etMobileNumber,
customer_row_, supplier_row_, etc.) inside the generic runner. None of
these selectors exist in the checked-in sample spec. Debug log now just
reports screen + hierarchy size; specs that want richer visibility can
log from state.ax.find themselves.
* build(sdk-android): bump AGP 8.11.0 → 8.13.0
Required by com.vanniktech.maven.publish 0.36.0, bumped in the
following commit to pull a Dokka 2.x that handles JDK 21+ version
strings.
* build(sdk-android): migrate to vanniktech 0.36 + Dokka v2 javadoc
Fixes the `javaDocReleaseGeneration` crash on JDK 21+ hosts
(IllegalArgumentException: 25.0.2 in the bundled IntelliJ
JavaVersion.parse). Root cause: AGP's JavaDocGenerationTask pulled
an old Dokka whose vendored util-lang predated JDK 9+ version
strings. Fixed upstream in Dokka 2.1.0 (Kotlin/dokka#4202).
Changes in this commit:
- Bump vanniktech-maven-publish 0.30.0 → 0.36.0, which drops Dokka
v1 support and the SonatypeHost parameter (Central Portal is
now the default).
- Apply org.jetbrains.dokka and org.jetbrains.dokka-javadoc 2.2.0
so the javadoc jar is generated by Dokka 2.x directly, not AGP's
bundled-Dokka path.
- Switch AndroidSingleVariantLibrary to the explicit
JavadocJar.Dokka("dokkaGeneratePublicationJavadoc") form; the
old `publishJavadocJar = true` boolean now maps to plain
javadoc, which still goes through AGP's broken path on Android
source sets.
- Enable V2Enabled in gradle.properties (required by 0.36+) and
silence the transitional opt-in warning.
Verified: `make release-android-local` on openjdk 25.0.2 produces
aar, sources jar, javadoc jar, pom, and module; `./gradlew
:sdk-android:testDebugUnitTest :sidecar:test` still pass.
* fix(cli): fall back to node_modules for @uatu/spec resolution
Drop the hard failure when the uatu source tree is not reachable from
the spec file. Users integrating uatu in their own app have @uatu/spec
installed via npm; esbuild now resolves it from node_modules.
* build(gradle): drop :sample-app include from root settings
The sample now has its own Gradle project in examples/sample-app/android.
* build(sample-app): vendor gradle wrapper
Users running the sample build the APK via ./gradlew from inside the
sample-app's own android/ directory, no repo-root wrapper required.
* build(sample-app): make gradle project standalone
Drop the project(':sdk-android') dependency in favor of the Maven
Central coordinate io.github.priyanshujain:sdk-android. The sample now
owns its settings.gradle.kts and gradle.properties, so it builds
without any pieces of the uatu source tree.
* chore(sample-app): declare @uatu/spec npm dependency
Mirrors what a downstream user would put in their own package.json.
Uses file: for pre-release development; becomes a normal semver pin
once @uatu/spec ships to npm.
* docs(sample-app): rewrite justfile and add README
Justfile drops repo_root; all recipes run against the local gradle
wrapper and uatu from PATH. README is scoped to what a user needs to
run the sample against their own device.
* docs(manual): update sample install steps to standalone layout
./gradlew :sample-app:installDebug no longer exists; the sample owns
its own wrapper under android/.
* feat(sdk): log when Uatu.start succeeds
Silent SDK start makes the "SDK didn't connect" failure mode
impossible to debug. One INFO line at start time is enough.
* fix(sidecar): launch via am start -W instead of monkey
monkey -p <pkg> -c LAUNCHER 1 is unreliable on API 36+: it reports no
error but silently fails to start the activity, so the SDK never runs
and the CLI times out on the SDK-accept handshake.
Resolve the launcher activity via `cmd package resolve-activity
--brief` and launch it with `am start -W -n`. -W makes the call block
until the activity is up, which also makes the subsequent SDK
accept timing deterministic.
* feat(cli): auto-resolve Android device; boot AVD if none connected
--avd becomes optional. Resolution order:
- use any already-connected adb device;
- else if --avd names an existing AVD, boot it and wait for boot;
- else if --avd is missing or names no AVD, error with a clear message.
Falls back to $ANDROID_HOME/emulator/emulator when the binary is not on
PATH, so a standard Android SDK install works without extra shell setup.
* docs(sample-app): AVD is optional; document both paths
just test runs against any connected device. If none, pass AVD=<name>
to have uatu boot the emulator for you.
* feat(cli): auto-discover Android SDK; auto-pick the lone AVD
adb and emulator are looked up via PATH, then $ANDROID_HOME,
$ANDROID_SDK_ROOT, ~/Library/Android/sdk, ~/Android/Sdk, and the
Homebrew cask path. The discovered platform-tools directory is
prepended to the sidecar's PATH so its adb subprocess calls work too.
When --avd isn't passed and no device is connected, the CLI picks the
sole local AVD and boots it. Multiple AVDs → error listing them.
* build(make): add `make install` that go-installs uatu onto PATH
Puts `uatu` into $GOBIN (or $GOPATH/bin) so the sample and any local
dev flow can call it without PATH= prefixes.
* docs(sample-app): zero-config just test; dotenv-load for persistence
Drop the expectation that users prefix commands with PATH=, ANDROID_HOME=,
or AVD=. `just test` now works as-is; optional knobs can be pinned in a
.env file alongside the justfile.
* fix(sample-app): auto-detect ANDROID_HOME for Gradle tasks
The Go CLI finds the SDK itself, but AGP still needs ANDROID_HOME to
resolve `sdk.dir`. The justfile now resolves it from env or canonical
install paths before invoking ./gradlew, so `just install` works out
of the box on a standard Android SDK setup.
* gitignore runs directory for sample app
* refactor(docs): inline pandoc build into Makefile, drop scripts dir
* fix(agent): wait for deadline watcher before returning
readWithDeadline's watcher goroutine could clobber the conn's read
deadline with time.Unix(1, 0) after the main function reset it to zero.
When the Accept ctx was canceled shortly after Accept returned, the
watcher raced with close(done) in select and sometimes picked ctx.Done()
even though we were already done reading, leaving the conn unusable for
the next read (instant i/o timeout on step 1 snapshot).
Synchronize on the watcher's exit before resetting the deadline so it
can never override the reset.
* test(agent): cover readWithDeadline race on Accept ctx cancel
Drives Accept with a short-timeout ctx, cancels it right after Accept
returns, then does a Snapshot. Reliably fails without the readWithDeadline
synchronization fix (watcher goroutine overwrites the deadline to past).
Targets: install/uninstall the APK, build the uatu CLI, run 'uatu test'
against a named AVD, run the Go verify tests, and clean. Keeps the
example self-contained so users can 'just test AVD=pixel_7' after cloning.
The old tests loaded merchant-android uiautomator dumps from /tmp and
skipped when absent. Replace with two hermetic tests that bundle
examples/sample-app/spec.ts against a synthetic hierarchy: one checks
tapClickMe fires, the other drives the three properties through a
holds/holds/violated snapshot sequence.
Introduce examples/sample-app/spec.ts — a minimal property-based spec that
taps the sample app's "Click me" button and asserts click_count is
monotonic. Bundle-check and the trace writer test now reference the new
path.
* feat(cli): add Version var and version subcommand
* build(gradle): introduce uatu.version property for lockstep releases
* build(sdk-android): swap GitHub Packages for vanniktech Maven Central plugin
* build(spec-api): make package publish-ready for npm
* ci(release): add goreleaser config for cross-platform uatu CLI builds
* ci: add ci and release GitHub Actions workflows
* ci: restrict ci.yml to PR + workflow_dispatch (no direct push to master)
* docs(release): add local release targets, env example, and install docs
* build(sdk-android): make signAllPublications conditional on signing key
* ci(release): stage sidecar JAR at embed path before go build
* chore(spec-api): regenerate package-lock for updated package.json
* ci: install protoc-gen-go plugins before buf generate
* ci: bump Node to 22 (required for --experimental-strip-types)
make uatu copies the real fat JAR into assets/ before
go build -tags withsidecar. Keeping that path tracked was
the root cause of the 130 MB push rejection.
Splits embed.go so the go:embed directive only fires under
-tags withsidecar. Default builds get a stub with a nil JAR
and IsPlaceholder()=true. This removes the landmine where
make uatu overwrote a tracked placeholder file, making any
git add silently stage 130 MB.
runTestPipeline assembles the v0.1 stack end to end:
1. Bundle the spec (with @uatu/spec alias resolution)
2. Extract the embedded sidecar JAR
3. Spawn java -jar sidecar --port <free>
4. Wait for sidecar Health
5. Listen on a host TCP port + adb reverse to the device's
localabstract:uatu-agent socket
6. Launch the app via the maestro driver
7. Accept the SDK HELLO
8. Load the bundle into the verifier
9. Open the trace writer + write meta.json
10. runner.Run for the requested duration
11. Terminate the app + clean up adb reverse
Test subcommand now prints a bundle error for a missing spec,
verifying the flag surface reaches the pipeline.
Removes Launch + Terminate from runner.Run so the CLI can launch
the app first, wait for the SDK to connect, then start the loop.
The previous shape forced runner to launch internally which fought
with the SDK-must-be-connected-first ordering.
BundleID/ClearState fields go away too since runner no longer
launches; the CLI keeps them on its testOptions struct.
Coordinates: dev.uatu:sdk-android:0.0.1. Credentials read from
GH_TOKEN/GH_USERNAME (or GITHUB_TOKEN/GITHUB_ACTOR for CI).
.env is gitignored so local tokens stay out of git.
Consumers add the maven repo + debugImplementation in their
build.gradle and we're done.
Property ledgerBalanceMatchesTxns asserts displayed balance equals
sum(Given) - sum(Received) on either ledger screen. Catches the
class of bug where the server-fed CustomerModel.balance diverges
from the local-DB-fed CoreDatabaseDao sums (stale cache, partial
sync, deleted-txn handling glitch, etc.).
Generators are gated by screen state and weighted to push the run
through login -> home -> ledger quickly:
enterPhone (100), enterOtp (100), openCustomerOrSupplier (80),
taps (10), swipes (2).
Phone/OTP read from process.env via esbuild defines so credentials
never land in source. cmd/internal-tools/bundle-check is a quick
sanity tool to confirm the spec bundles before running uatu test.
Doctor flags a shipped binary that's still carrying the build-time
placeholder so `uatu test` won't silently fail trying to launch a
nonexistent sidecar. Makefile uatu target now copies the freshly
built fat JAR into the embed directory before `go build`.
Ships with a 24-byte placeholder so fresh clones build without
requiring a sidecar build first. `make uatu` copies the real
fat JAR into internal/sidecar/assets before `go build`, so
shipping binaries carry the full sidecar (~130 MB).
Extract writes the JAR to a temp dir alongside a SHA-256 file
and skips rewrite when the checksum already matches.
Wraps each v0.1 RPC, plus a WaitForHealth helper that polls until
the sidecar reports Ready=true (used at startup before any other
calls happen). Tests stub the gRPC server in-process so they don't
need a real sidecar JAR.
Driver interface in Go already exposes TapSelector for selector-based
taps; mirror that in the proto so maestro can resolve selectors
sidecar-side rather than forcing Go to walk the hierarchy first.
DriverService delegates each RPC to a pluggable DriverBackend so
real Maestro integration can land in a follow-up without changing
the gRPC layer. StubDriverBackend satisfies the contract and lets
the Go side talk to a running sidecar today.
Generates Java stubs from proto/driverpb/driver.proto via the
protobuf plugin (sourceSets points at the shared root proto dir).
Pulls in dev.mobile:maestro-client:1.40.0, grpc-netty-shaded,
slf4j-simple, and the JUnit vintage engine so the gRPC test rules
work alongside JUnit Jupiter.
Inspector parses uses-permission entries from `aapt dump permissions`
and the granter shells out to `adb shell pm grant`. Both are pluggable
so tests can drive logic without aapt or a device. Granter failures
become warnings rather than errors — Android refuses non-runtime
permissions and we'd rather keep going than abort the run.
Each check is a value so tests can swap in fakes. Java check parses
both legacy (1.8) and modern (17+) version strings. Emulator check
falls back to ANDROID_HOME/emulator/emulator since the brew cask
ships it without putting it on PATH.
Wires agent.Conn + driver.Driver + verifier.Verifier + trace.Writer
into the v0.1 step cycle: snapshot the SDK, push to verifier,
evaluate properties, write the trace step (with violations), release
the SDK pause, apply the next action via the driver, wait for idle.
Driver.Launch happens once before the loop and Terminate runs in
defer so even an early error tears down the app cleanly. Summary
returns step count and per-step violation records for the caller
to print or persist.
Real maestro driver will resolve selectors itself rather than
forcing the runner to look up coordinates from the hierarchy. Mock
now waits the requested duration so tests don't busy-spin and
starve the SDK fixture goroutine.
Installs globalThis.__uatu__ with extract, always, actions,
weighted, tap, inputText, and stub taps/swipes. Load runs the
bundled spec, then pulls properties + actions out of globalThis.
PushSnapshot rebuilds state.snapshots and refreshes every
extractor handle's current/previous in registration order so
chained extractors observe up-to-date values.
Properties are wired through internal/ltl as Always(Thunk(...)),
so verdicts latch to violated as soon as a predicate returns
false. NextAction resolves actions/weighted recursively with a
seedable rand source for reproducible runs.
Thin wrappers over a globalThis.__uatu__ runtime that the verifier
provides as host bindings. Spec authors get IntelliSense for
extract/always/Tap/InputText/actions/weighted/taps/swipes, but the
bundle stays small because runtime logic lives in goja.
TypeScript 5.6 with strict mode, bundler module resolution. Tests
run via node --test --experimental-strip-types so we don't need
vitest or a build step. Adds node_modules to .gitignore.
Each WriteStep appends one JSON object per line so the trace can
be filtered with jq directly. Screenshots land under screenshots/
with zero-padded indexes. Writer is concurrency-safe; Close is
idempotent and a write after Close errors loudly rather than
silently dropping.
Supports Always over Pure/Thunk leaves; eventually/next/bounds
deferred. Once a thunk returns false under an Always, the verdict
latches to violated so the runner can surface the offending step
without later observations masking it.
Bundles a TypeScript entry into an IIFE ES2020 blob with optional
inline sourcemaps. process.env defines are JSON-quoted before being
fed to esbuild so values with quotes/backslashes survive correctly.
Returns the bundle's SHA-256 for trace meta.json.
Records every call as an Action and lets tests program hierarchy,
screenshot, health, and per-method failures. Actions() returns a
copy so test code can't mutate the driver's history.
Choreographer.getInstance() is per-thread and throws on threads
without a Looper. The previous impl called it from the SocketClient
reader thread, which surfaced as snapshot failures with empty
state during the sample-app round trip. Now the reader thread
posts a Runnable to the main Handler that, on the main thread,
gets the Choreographer and posts the frame callback.
Listens on TCP, runs adb reverse to expose the host port as the
device's localabstract:uatu-agent socket, then drives N PAUSE/STATE
/RESUME cycles against the connected SDK. Cleanly removes the adb
forward on exit.
Single-activity Android app that calls Uatu.start() in
SampleApplication.onCreate and registers three extractors
(app_state, click_count, uptime_millis). MainActivity has a
button so click_count exercises non-trivial state.
Uatu singleton owns an internal UatuRuntime that wires the socket
client, pauser, and extractor registry. start() sends HELLO on
connect, responds to PAUSE by running extractors inside the pause
window and replying with STATE, and releases the gate on RESUME.
LinkedHashMap preserves extractor registration order so trace
output is deterministic. Failed extractors log a warning and
record null rather than aborting the whole snapshot.
Worker thread calls pauseAndSnapshot with an extractor closure;
the Pauser posts a frame callback that runs extractors on the
main thread and then blocks that thread on a semaphore until
release() fires. FrameCallbackPoster abstracts Choreographer so
JVM unit tests drive it with a single-thread executor, while the
Android runtime uses ChoreographerPoster.
SocketClient owns a background reader thread, dispatches incoming
messages to a handler, and auto-reconnects with exponential backoff
when the transport drops. AgentTransport abstracts the socket so
JVM unit tests drive the client through piped streams while the
Android runtime uses LocalSocket over the abstract namespace.
Send is exposed to consumers via a MessageSender passed to
onConnected. Writes are serialized under the output stream lock.
Accept waits for an SDK HELLO then hands back a Conn. Conn.Snapshot
sends a PAUSE, blocks on the matching STATE (id-correlated), and
leaves the SDK paused until Release sends RESUME. Conn.Close sends
GOODBYE best-effort.
v0.1 supports one client at a time; transport is left to the caller
so tests can use TCP loopback while production wires via adb reverse
to localabstract:uatu-agent.
Uses org.json (already on Android; org.json:json test dep for JVM
unit tests). Field names and framing mirror internal/agent/protocol.go
so Go-encoded frames decode on the SDK without a shared schema file.
Kotlin 2.1.21 (deviation from plan's 1.9, forced by Gradle 9
compatibility), JDK 17 toolchain, application plugin with main
entry dev.uatu.sidecar.MainKt. Shadow 9.0.0-rc2 for the fat JAR.
Actual gRPC server wires in with task #10.
Wires the flag surface from plan §3 (--spec, --bundle-id, --platform,
--avd, --duration, --seed, --output). Subcommand bodies are stubs;
actual work lands with runner/doctor tasks.