Commit Graph
97 Commits
Author SHA1 Message Date
pj 36188ca906 test+refactor: real sidecar test, deterministic test sleeps, slog step line (#22)
* test(sidecar): replace assertTrue(true) placeholder with real server test

MainTest.mainExists() always passed and inflated the green-check count.
DriverServiceTest covers RPCs, but SidecarServer start/stop had no
coverage. Drop the placeholder and add SidecarServerTest that binds to
port 0, asserts a real ephemeral port, and stops cleanly.

* test(agent): drop 50ms sleep before cancel in TestServer_AcceptCancelsOnContext

Accept's closeListenerOnCancel watcher closes the listener as soon as
ctx fires, regardless of whether the outer Accept has reached
listener.Accept() yet. The sleep was a CI-flake surface (50ms is not
enough on a slow runner), and dropping it still exercises the same
outcome — Accept returns with ctx.Err() after cancellation.

Stable across 50x -count runs.

* test(agent): replace 2s sleep with done-chan in TestConn_SnapshotTimesOutIfSDKSilent

The silent-SDK fake held the connection open via time.Sleep(2s), which
coupled the test's wall clock to the server's 200ms snapshot-timeout
assertion. Swap for a done channel closed by t.Cleanup — the goroutine
exits when the test ends, independent of timing.

* test(maestro): make WaitForHealth_PollsUntilReady deterministic

Replace the 50ms wall-clock sleep that flipped healthReady with a
healthReadyAfterCall counter in the fake server. The handler returns
ready=true once healthCalls reaches the threshold, so the test's
"at least 2 polls before ready" assertion is satisfied by call
count rather than a race between the flip goroutine and the 25ms
poll loop.

* refactor(runner): route per-step progress through slog instead of fmt.Printf

The runner already carries a *slog.Logger for warnings (logger.Warn on
decode failures, predicate errors). The per-step status line was the
outlier — a bare fmt.Printf that wrote to os.Stdout unconditionally,
bypassing both the injected logger and any caller-configured writer.

Switch it to logger.Info("step", "index", ..., "screen", ..., "nodes", ...).
The caller (cmd/uatu) is responsible for wiring a logger whose handler
renders to the right stream; the next commit adds that wiring.

* feat(cli): render runner progress via a thin slog handler on stdout

progressHandler writes Info records as "msg key=value ..." and prefixes
warnings/errors with their level, matching the prose style of the
surrounding CLI status prints. Wired into the runner via
runner.Options.Logger so the per-step status line still lands on stdout
without slog's default time= / level= framing.
2026-04-20 17:58:41 +07:00
pj 323878c34a fix(verifier): don't crash on throwing JS predicates (#21)
* fix(verifier): don't crash on throwing JS predicates

formulaThunk used to panic whenever goja returned an error from a
predicate callable, and nothing on the LTL -> runner path recovered, so
a malformed spec (e.g. a property whose body throws or touches an
undefined field) would kill the verifier process.

Latch the first error on formulaState, return false so LTL marks the
property violated, and expose PredicateError(name) that walks the
property's formula-spec tree and surfaces the latched cause.

* fix(runner): log predicate errors alongside violations

For each violated property, surface the verifier's latched predicate
error via logger.Warn so operators can distinguish a genuine false
verdict from a malformed spec. Add a runner-level test asserting that a
throwing predicate no longer crashes the run and that the error message
appears in the log.
2026-04-20 16:55:10 +07:00
pj a2e96af1af WIP: rename sample app to Folio (#20)
* refactor: rename examples/sample-app to examples/folio

Directory-level rename and path references in Go tests, bundle-check,
top-level README, and getting-started docs. Package declarations,
Gradle config, iOS bundle IDs, and class names follow in later commits.

* refactor(folio): rename Kotlin package dev.uatu.sample to app.folio

Moves source dirs and sqldelight schema from dev/uatu/sample to
app/folio, updates package declarations and imports, and switches
Android namespace/applicationId, iOS binaryOption bundleId, and
sqldelight database packageName to the new identifier.

* refactor(folio): rename SampleApplication to FolioApplication

Android manifest now points at .FolioApplication with label 'Folio'
instead of 'Uatu Sample'.

* refactor(folio): set iOS bundle id and display name to Folio

bundleIdPrefix + PRODUCT_BUNDLE_IDENTIFIER -> app.folio.
CFBundleName + CFBundleDisplayName -> 'Folio'.

* refactor(folio): update demo email to [email protected]

* refactor(folio): point justfile at app.folio bundle id

Updates xcrun simctl launch target, uatu test --bundle-id, and the
build/uninstall comments to reference folio instead of sample.

* test: update fixture package ids to app.folio

Sidecar activity-resolver test and verifier spec-integration XML
fixtures referenced the old dev.uatu.sample Android package. Updates
them to match the folio app's real package id so the tests stay
representative of what the CLI sees on-device.

* test(verifier): rename SampleApp identifiers to Folio

Renames TestSampleAppSpec* functions, bundleSampleAppSpec helper, and
sampleAppHierarchyXML const (now loginHierarchyXML for consistency with
the other per-screen fixtures). Updates trailing sample-app mentions in
comments and assertion messages.

* refactor(folio): rename Gradle/npm/wasm project identifiers to folio

settings.gradle.kts rootProject.name, package.json + package-lock.json
name, and the WasmJS index.html <title> all still read 'uatu-sample' /
'Uatu Sample'. Realigns them with the Folio brand.

* docs(folio): rewrite README title + getting-started bundle id

examples/folio/README.md is now titled 'Folio' with the Kotlin source
paths corrected to app/folio. Getting-started example uses --bundle-id
app.folio. Harness launch message is now generic ('app under test')
since uatu-sample-harness is not specific to folio.

* chore(folio): drop trailing 'sample' reference in gradle.properties

* refactor(folio): rename LoginPage composable to LoginScreen

Align with KMP/Android industry convention (NowInAndroid, Cash App,
JetBrains samples use Screen, not Page).

* refactor(folio): rename HomePage composable to HomeScreen

* refactor(folio): rename AddAccountPage composable to AddAccountScreen

* refactor(folio): rename LedgerPage composable to LedgerScreen

* refactor(folio): rename AddTransactionPage composable to AddTransactionScreen

* refactor(folio): split Models.kt into app.folio.data package

Account, Transaction (with TxnType), and Session move into their own
files under app.folio.data, matching NowInAndroid-style per-type
organization.

* refactor(folio): move data layer into app.folio.data package

Repository, LedgerStore (expect + interface), SqlLedgerStore,
WebLedgerStore, DriverFactory (expect + actuals), AndroidLedgerContext,
and Snapshot move into app.folio.data. Update all consumer imports.

* refactor(folio): move Navigation into app.folio.navigation package

Split the former Navigation.kt into Route.kt (sealed interface) and
Navigator.kt (singleton). Update consumer imports across screens,
App.kt, and FolioApplication.

* refactor(folio): move Platform and Format into app.folio.platform

Both files carry expect declarations (Platform object, formatDate);
grouping them into a dedicated platform package makes the KMP seam
obvious and mirrors the structure used by JetBrains samples.

* refactor(folio): move login into feature/auth package

Create app.folio.feature.auth with LoginScreen + LoginUiState. Inline
the former Auth.kt (DEMO_EMAIL, DEMO_PASSWORD, checkCredentials) into
LoginScreen since it is the sole caller.

* refactor(folio): move HomeScreen into feature/home package

* refactor(folio): move account creation into feature/account package

AddAccountScreen gets its own AddAccountUiState colocated with the
screen, replacing the shared UiState.addAccountError.

* refactor(folio): move ledger screens into feature/ledger package

LedgerScreen and AddTransactionScreen move into app.folio.feature.ledger
with AddTransactionUiState (txnError, txnFormType) colocated. The
former catch-all UiState.kt is removed now that each screen owns its
state alongside its UI.

* refactor(folio): split Theme.kt; move theme and icons to subpackages

Theme split into Theme.kt (tokens, layout dims, LedgerTheme) and
Type.kt (typography) under app.folio.ui.theme. Icons moves to
app.folio.ui.icon. Update every consumer's imports to match.

* refactor(folio): split ui components into per-file under ui/component

Former Widgets.kt and Components.kt become 10 focused files: AppButton,
Card, EmptyState, ErrorText, FieldLabel, Header, IconButton (w/
BackButton), Screen, Segmented, TextInput. Matches NowInAndroid style
of one composable per file in a designsystem/component package.

* chore(folio): consolidate uatu testing files under uatu/ folder

Move spec.ts, package.json, package-lock.json into examples/folio/uatu
so all uatu-specific testing artifacts live in one place. runs/ and
node_modules/ follow the same convention (both remain gitignored).
Update justfile, README, and the two Go consumers (bundle-check tool +
verifier/trace tests) that referenced the old path.

* refactor(trace): drop folio path in writer test

Round-trip only needs a non-empty string; neutralize to keep the
library free of folio references.

* refactor(sidecar): neutralize ResolveActivity test fixtures

Swap app.folio for com.example.app in the fixture strings so the
sidecar tests don't reference the example app by name.

* refactor(bundle-check): take spec path as argument

Previously the tool hardcoded examples/folio/uatu/spec.ts. Accept a
positional spec path instead so the tool works for any example and
leaves no folio reference in the library surface.

* test(verifier): add neutral integration spec and hierarchy fixtures

Adds testdata/integration_spec.ts with two routes ("list", "form"),
an InputText on text_field, a Tap on primary/secondary_action, a
safety property (itemCountNonNegative), and a liveness property
(submitEventually). Adds hierarchies_test.go with matching XML
fixtures. Constants intentionally go in a _test.go at package root
rather than testdata/hierarchies.go because go skips .go files
under testdata/.

* refactor(verifier): replace folio integration tests with neutral ones

Renames bundleFolioSpec -> bundleIntegrationSpec and the three Test*
entry points to TestIntegrationSpec*. Uses the synthetic spec and
hierarchies added in the previous commit so the library's test suite
no longer references examples/folio at all.

Folio-specific coverage remains covered by examples/folio/justfile's
'just test' which exercises the real spec on device/emulator.

* chore: remove cmd/uatu-sample-harness

Not referenced by Makefile, docs, CI, or any script. Duplicates the
adb reverse helpers already in cmd/uatu/test_run.go, and its name
implies ownership by the sample app which violates the library/
example decoupling. If a bare-protocol debugging tool is later
needed it belongs inside cmd/uatu/.

* docs(folio): drop Layout section and KMP layout paragraph; fix AVD override syntax

The directory-tree Layout section rots faster than the code and
duplicates what ls shows for free. The expect/actual paragraph in
Stack was the same kind of filler. The README also claimed 'just
AVD=Pixel_7 test' but the justfile reads AVD as an env var via
env_var_or_default, so the correct invocation is 'AVD=Pixel_7
just test'.
2026-04-20 16:04:20 +07:00
pj c1de4bf57a fix(sample-app): address PR #18 review findings (#19)
- WebLedgerStore.accountExistsByName case-insensitive (matches SQL COLLATE NOCASE)
- drop SampleApplication.maybeInjectDebugError; sample spec now passes clean
- drop noLogcatErrors from sample spec (default matches system-wide E logs)
- openRandomAccount picks uniformly from findAll instead of first match
- clear txnError on any add-transaction interaction, not only valid amount input
- escapeForAdbInputText quotes shell metacharacters (quotes, backslash, etc.)
- extract buildClearKeyevents helper with empty/normal/cap tests
- broaden spec_integration_test.go to cover home, add-account, ledger,
  add-transaction action generators
- document FocusTracker single-focus invariant
2026-04-20 13:31:50 +07:00
pj 8381a98aaf feat(sample-app): ledger spec parity (#18)
* feat(sample-app): add FocusTracker

* feat(sample-app): support description on TextInput

* feat(sample-app): support description on AppButton and Segmented

* feat(sample-app): stable ids on login screen

* feat(sample-app): stable ids on add-account screen

* feat(sample-app): stable ids on add-transaction screen

* feat(sample-app): stable ids on home and ledger screens

* feat(sample-app): hoist error + form state into UiState

* feat(sample-app): register auth_status + accounts snapshots

* feat(sample-app): register ledger_rows + ledger_balance snapshots

* feat(sample-app): register error + focused_input snapshots

* refactor(sample-app): scaffold spec.ts extractors + safety

* feat(sample-app): spec accounting invariants

* feat(sample-app): spec state-machine monotonicity

* feat(sample-app): spec liveness properties

* feat(sample-app): spec auth + account action generators

* feat(sample-app): spec transaction action generators

* feat(sample-app): spec weighted workflow

* feat(sample-app): publish login form input snapshots

* feat(sample-app): publish account + txn input snapshots

* feat(sample-app): register form input snapshots

* fix(sample-app): sequence login + idempotent text inputs in spec

* fix(sample-app): clear UiState on page dispose

* fix(sample-app): tighten adversarial login, allow retype on error

* test(verifier): update sample-app spec integration for new selectors

* feat(sidecar): clear focused field before InputText types

* refactor(sample-app): simplify loginHelper to focus-driven sequencing

* refactor(sample-app): drop input-value UiState mirrors

* refactor(sample-app): drop input-value snapshot registrations

* test(verifier): adjust sample-app integration for replace-on-type
2026-04-20 13:28:41 +07:00
pj 7493945251 feat: LTL operators, sampling, and default generators (#17)
* feat(ltl): add Now/Next/Eventually/Implies/Or/And/Not formulas

Replace the fold-with-latch evaluator with a residual-formula reducer.
Each Observe() instantiates a fresh obligation from the root (stripping
an outer Always), reduces each pending obligation against current state,
latches Violated on first failure, and surfaces Pending verdicts for
deferred obligations. Existing Always/Pure/Thunk tests continue to pass.

* feat(ltl): support relative duration for eventually().within()

* feat(proto): add Swipe, PressKey, RecentLogs RPCs

* feat(verifier,runner): formula handles, new action kinds, rich state

- verifier: add formula-spec registry; bindNow/bindNext/bindEventually with
  chainable .implies/.or/.and/.not and .within(n,unit) on eventually; bindFrom
  for uniform sampling. bindAlways keeps accepting plain predicates.
- verifier: store lastTree, lastAction, step time, logs, exceptions on the
  Verifier; SnapshotInput replaces the (snapshots, tree) pair. stateObject now
  produces state.lastAction/time/logs/exceptions matching the TS State type.
- verifier: make taps/swipes/waitOnce/pressKey built-in generators actually
  fire; taps picks a clickable, enabled element from the last hierarchy.
- agent: add exceptions field to Message wire format.
- driver: add Swipe/PressKey/RecentLogs to Driver interface; wire maestro
  client and mock driver. LogEntry exposed for runner consumption.
- runner: apply Swipe/PressKey/Wait actions; collect logcat and exceptions;
  pass lastAction and step time into PushSnapshot.

* feat(spec-api): LTL operators, new actions, richer State

- ltl.ts exports now/next/eventually; always overload accepts a Formula
- types.ts: Formula gains implies/or/and/not; EventuallyFormula adds .within;
  State gains lastAction/time/logs/exceptions; Swipe/PressKey/Wait action types
- actions.ts: Swipe/PressKey/Wait/from constructors; waitOnce + pressKey
  default generators
- tests exercise the chaining, sampling, and new actions through a recorded
  fake runtime

* feat(sidecar): add swipe, pressKey, recentLogs RPC handlers

* feat(sdk-android): capture uncaught exceptions

Install a default uncaught handler on Uatu.start, chained with any
existing handler so Android's crash reporter still runs. Expose
Uatu.reportError for callers to forward caught throwables. A bounded
circular buffer (default 50) drains into each STATE message's new
exceptions field. Protocol.kt serializes/deserializes the field,
matching the Go wire format added to internal/agent/protocol.go.

* feat(spec-api): add @uatu/spec/defaults/properties bundle

* feat(sample-app): exercise new LTL operators + defaults

spec.ts now imports eventually/next/now/from from @uatu/spec and
noUncaughtExceptions from @uatu/spec/defaults/properties. It declares
three properties that exercise the new surface:

- accountCountNonNegative: plain always() safety
- addAccountAdvances: always(now(x).implies(next(y)))
- eventuallyLoggedIn: eventually(p).within(30, "seconds")
- noUncaughtExceptions: imported default

The weighted actions root uses from() for random phone/name sampling
and entries for taps/swipes/waitOnce/pressKey built-ins.

SampleApplication gains a debug hook gated on the system property
uatu.inject_error so the e2e run can synthesize an Uatu.reportError and
verify noUncaughtExceptions violates.

cmd/uatu/test_run.go adds a subpath alias so specs importing
"@uatu/spec/defaults/properties" resolve against the in-tree source
when running from the uatu checkout. The spec-integration tests swap
the old click-counter fixtures for the new login hierarchy.

* feat(trace): record swipe/key/wait details + exceptions

trace.Step gains an Exceptions array so the trace captures the
class/message/stackTrace for each SDK-reported throwable in a step.
trace.Action gains FromX/FromY/ToX/ToY/Key/DurationMillis so the full
payload of Swipe/PressKey/Wait actions is visible in trace.jsonl.

sample-app's debug error hook now gates on ApplicationInfo.DEBUGGABLE
instead of a system property (adb setprop fails on non-rooted
emulators).
2026-04-20 02:19:39 +07:00
pj 5b5594ee05 Port sample-app to KMP with Android, iOS, Web, and SQLite (#16)
* chore(sample-app): hoist gradle wrapper to sample-app root

* chore(sample-app): add KMP root gradle config

* chore(sample-app): add composeApp KMP module build config

* chore(sample-app): add Android manifest for composeApp

* feat(sample-app): add shared domain models and auth constants

* feat(sample-app): add shared number and date formatting

* feat(sample-app): add cross-platform storage, clock, and id

* feat(sample-app): add shared repository with file-backed state

* feat(sample-app): add shared in-memory navigator

* feat(sample-app): add Compose theme and design tokens

* feat(sample-app): add shared UI components (icons, screen, widgets)

* feat(sample-app): add Login and Home pages

* feat(sample-app): add AddAccount, Ledger, AddTransaction pages

* feat(sample-app): add App root composable with routing

* feat(sample-app): add Android Application and Activity hosting Compose UI

* chore(sample-app): remove legacy android module (replaced by composeApp)

* chore(sample-app): bump to latest stable Kotlin/AGP/Compose deps

* fix(sample-app): make iOS compile (drop @Volatile, set bundleId)

* feat(sample-app): add xcodegen spec, SwiftUI host, and iOS Info.plist

* chore(sample-app): ignore build artifacts and generated xcodeproj

* chore(sample-app): update justfile for composeApp layout, add ios target

* docs(sample-app): rewrite README for KMP + iOS flow

* refactor(sample-app): drop in-app status bar and formatClock

* feat(sample-app): add SQLDelight schema and per-platform drivers

* refactor(sample-app): back Repository with SQLite, drop file serializer

* feat(sample-app): wire native back on Android and iOS edge swipe

* feat(sample-app): semantic roles, labels, and a11y descriptions

* fix(sample-app): link libsqlite3 for iOS target

SQLDelight's native driver needs libsqlite3.tbd on iOS; without it the
linker fails with undefined _sqlite3_bind_blob and friends.

* refactor(sample-app): abstract storage behind LedgerStore interface

Platform-specific createLedgerStore() returns a SqlLedgerStore backed
by SQLDelight on Android + iOS. Opens the door for a pure in-memory
web implementation that does not require a SQLite driver.

* feat(sample-app): add wasmJs target with in-memory LedgerStore

Wires a Compose Multiplatform browser canvas entry point. The web
implementation of LedgerStore is an in-memory model with localStorage
persistence, so it does not need a SQLite driver. Back navigation maps
the browser back button to the same BackHandler contract Android and
iOS use.

* chore(sample-app): settings + gitignore for wasmJs dev run

Registers the Node.js distributions repository and switches
repositoriesMode to PREFER_PROJECT so the Kotlin wasmJs plugin can
download its toolchain. Adds kotlin-js-store (lockfile) and ignores
runs/, web screenshots, playwright-mcp scratch output.

* fix(sample-app): singularize transaction count on Home

Shows "1 transaction" not "1 transactions" for accounts with a single
transaction; falls back to "$count transactions" otherwise.
2026-04-19 15:56:33 +07:00
pj 69002b07b1 fix(runner,sample-app): surface silent errors and demo a failing property (#15)
* fix(runner): surface non-deadline WaitForIdle errors

Previously the WaitForIdle return value was discarded entirely, hiding
real driver failures (gRPC transport errors, sidecar crashes) behind
the expected deadline-exceeded case. Log non-deadline errors so they
are visible without changing control flow.

* chore(sample-app): drop unused uptime_millis extractor

Registered in SampleApplication but never consumed by spec.ts.

* fix(sample-app): drop trivial appIsRunning property

app_state was hardcoded to 'running' so the property was a tautology
that could never fail. Removing both the extractor and the property
is the simplest fix; demo-grade properties that can fail land next.

* feat(sample-app): add Reset button that zeroes clickCount

Pairs with the next commit's tap-reset action so the fuzzer can
violate clickCountNeverDecreases and demonstrate uatu actually
finding a property violation.

* feat(sample-app): add tap-reset action to exercise Reset button

Weighted at 10/122, fuzzer reaches it within a short run. Pairs with
the Reset button to demonstrate uatu detecting the
clickCountNeverDecreases violation.

* fix(runner): filter WaitForIdle errors via context state, not errors.Is

errors.Is(err, context.DeadlineExceeded) misses gRPC's wrapped
status.DeadlineExceeded, so every step under the maestro driver
logged a spurious warning. Check idleCtx.Err() instead — captures
both deadline-fired and parent-canceled cases regardless of how the
driver wraps them.

* chore(sample-app): tune action weights so demo violates in ~30s

Prior weights left tap-reset rare enough that short demo runs missed
the violation by chance. Bumped to 30/107, with typeUsername reduced
since username noise doesn't help exercise clickCount.

* refactor(runner): route warnings through slog

Adds Options.Logger (defaults to slog.Default()) and converts the
three warning sites that were using fmt.Printf. Progress line stays
on Printf since it's user-facing UI, not a log. Makes the warnings
testable via a capturing handler.

* test(runner): assert WaitForIdle driver errors are logged

Captures slog output via TextHandler into a buffer and asserts the
warning message + injected error text appear when the mock driver
returns a non-context error from WaitForIdle. Guards against a
regression of the silent-error swallow.
2026-04-18 18:48:25 +07:00
pj 00f66ba48d fix: pre-v0.1 review feedback (#14)
* fix(sdk-android): add @JvmOverloads to Uatu.start

Java callers can now invoke start(application) without supplying a
Configuration, matching the Kotlin default-arg ergonomics.

* feat(agent): add protocol_version to HELLO handshake

ProtocolVersion=1 lives on Message and is set by Hello(). Server.Accept
rejects mismatches with a clear error. SDK upgrades that don't change
the wire format keep the same protocol_version; bump on breaking changes.

* test(agent): assert protocol_version in Hello round-trip

* feat(sdk-android): send protocol_version=1 in HELLO

Mirrors agent.ProtocolVersion on the Go side. Bump in lockstep with
the Go constant when the wire format breaks.

* chore(sample-app): pull @uatu/spec from npm next tag

Replaces the file: dep. Copy-paste users can now npm install against
the registry. The release workflow publishes pre-release tags to
npm dist-tag 'next', so the sample tracks the latest rc without
manual version bumps. Lockfile currently resolves to 0.0.1-rc3.
2026-04-18 18:00:12 +07:00
pj d0578dbaaa fix(runner): warn on malformed screen snapshot (#13)
* fix(runner): warn on malformed screen snapshot

screenFromSnapshot swallowed json.Unmarshal errors, so a non-string
screen value silently became "" in the step log and trace while the
verifier still saw the raw JSON. Return the error and warn at the
call site, matching the hierarchy warning pattern.

* docs: clarify --avd is optional for uatu test

The CLI accepts --avd as an empty-string default (cmd/uatu/main.go:49)
and only requires it when no device is connected and multiple AVDs
exist (cmd/uatu/android_env.go:63). Docs and examples that showed it
as required or always-passed were misleading.
2026-04-18 17:14:31 +07:00
pj 16e55086d8 fix(runner): surface focus-tap errors in InputText (#12)
* fix(runner): surface focus-tap errors in InputText action

A failed Tap/TapSelector before InputText was swallowed, so text typed
into the wrong field (or no field) still reported success. Return the
error so the step fails explicitly.

* feat(sample-app): add username EditText and snapshot

Gives the spec a real EditText target (content-desc: username_field)
so the InputText action path can be exercised end-to-end. The typed
value is mirrored into MainActivity.username and surfaced as the
"username" snapshot for spec assertions.

* feat(sample-app): exercise InputText action against username field

Adds typeUsername action and usernameNeverShrinks property to the
sample spec, and extends the integration test to assert the bundled
spec emits an InputText(desc:username_field, "alice") action and that
the property correctly violates when a snapshot reports a shorter
string.
2026-04-18 16:51:35 +07:00
pj b457e22569 refactor(runner): drop hardcoded per-app selectors from step log (#10)
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.
2026-04-18 16:41:12 +07:00
pj 3b0393c2bb fix(sdk-android): unblock publishToMavenLocal on JDK 21+ hosts (#9)
* 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.
2026-04-18 15:53:47 +07:00
pj 5dbadc4110 Delete bug.md as it got pushed by accident 2026-04-18 15:36:59 +07:00
pj 6b1c17328c fix(sample-app): make it standalone, no repo_root assumptions (#8)
* 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
2026-04-18 15:31:47 +07:00
pj bf733f4f93 Revise doc links in README
Updated documentation links to point to the new site.
2026-04-18 14:18:35 +07:00
pj ae595526da fix(agent): race in readWithDeadline clobbers conn deadline (#7)
* 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).
2026-04-18 14:16:15 +07:00
pj 55ef13e0ed refactor(docs): inline pandoc build into Makefile (#6)
* refactor(docs): inline pandoc build into Makefile, drop scripts dir

* ci(docs): use make target in workflow, drop scripts path trigger
2026-04-18 14:06:53 +07:00
pj e62319e916 docs: pandoc-based site and v0.1.0 groundwork (#5)
* chore(prose): remove em-dashes from config files

* chore(prose): remove em-dashes from android sdk config

* docs(spec-api): remove em-dash from README

* fix(doctor): reword sidecar-jar error without em-dash

* test(sidecar): reword assertion message without em-dash

* docs: add CLAUDE.md with project conventions

* build: add docs target for pandoc site

* docs(site): add pandoc template and stylesheet

* docs(site): add pandoc build script

* docs(site): add landing pages

* docs(manual): add getting-started

* docs(manual): add writing-specs

* docs(manual): add runs

* docs(manual): add cli reference

* docs(dev): add design principles

* docs(dev): add architecture

* ci: deploy docs site to github pages

* docs: rewrite README as entry point to docs site
2026-04-18 14:00:57 +07:00
pj a74d9fbeae build(examples): add justfile for sample-app
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.
2026-04-18 10:33:53 +07:00
pj ec3764ad85 chore(examples): remove merchant-ledger spec
No longer referenced anywhere — sample-app replaces it as the
in-tree example.
2026-04-18 10:33:49 +07:00
pj 40c92d4569 test(verifier): rewrite integration tests for sample-app spec
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.
2026-04-18 10:33:43 +07:00
pj 0c775c199d feat(examples): add sample-app spec
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.
2026-04-18 10:33:35 +07:00
pj c12a3e242f refactor(android): move sample app under examples/sample-app/
Rename the gradle sample project from :sdk-android-sample to :sample-app
so the isolated example for users lives beside its spec in examples/.
2026-04-18 10:33:31 +07:00
pj f57634809e build(sidecar): pin shadowJar output name to sidecar-all.jar (#3) 2026-04-18 09:05:00 +07:00
pj 2b006aabd8 fix(spec-api): use tsx loader so tests run on Node 20 (#2) 2026-04-18 08:56:27 +07:00
pj 0570719e6f Publish pipeline: goreleaser + Maven Central + npm (#1)
* 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)
2026-04-18 08:50:57 +07:00
pj 6e4c832678 chore: stop tracking sidecar JAR (build artifact)
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.
2026-04-18 06:46:32 +07:00
pj ebb9389754 build(make): pass -tags withsidecar when embedding real JAR
Also mkdir the assets dir so fresh clones work once the JAR
path is gitignored in the next commit.
2026-04-18 06:46:28 +07:00
pj ae10354ff0 test(sidecar): split tests across stub and withsidecar builds
Default !withsidecar build: assert IsPlaceholder, empty JAR,
Extract errors. withsidecar build: existing extract/checksum
coverage.
2026-04-18 06:46:24 +07:00
pj 4744736dc5 refactor(sidecar): gate JAR embed behind withsidecar build tag
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.
2026-04-18 06:46:20 +07:00
pj 6a07284e24 fix(spec): screen-aware sign convention + leaveLedger generator to cycle counterparties 2026-04-18 06:39:00 +07:00
pj b19d83fd54 fix(spec): state-aware enterMobile + globalise properties for evaluator pickup 2026-04-18 06:39:00 +07:00
pj f2fe54debc test(verifier): verify dismissMultiDevice fires against real confirm dialog 2026-04-18 06:39:00 +07:00
pj 4b43b9a22a chore(runner): log screen + tag hits per step for debuggability 2026-04-18 06:39:00 +07:00
pj cefb398437 feat(verifier): retry NextAction up to 16x so gated generators eventually fire 2026-04-18 06:39:00 +07:00
pj 6c7ea35a7d feat(hierarchy): expose checked/focused/selected on element objects 2026-04-18 06:39:00 +07:00
pj b248ad2e5a test(verifier): integration tests against live uiautomator dumps 2026-04-18 01:59:54 +07:00
pj 6ce4eabe86 feat(spec): drive merchant onboarding (English, phone, OTP, multi-device, home) 2026-04-18 01:59:51 +07:00
pj eea9a0067a feat(hierarchy): descPrefix selector for Compose testTag + UUID suffixes 2026-04-18 01:59:46 +07:00
pj fb6c3ca517 fix(runner): fetch hierarchy before SDK pause to avoid stale uiautomator dumps 2026-04-18 01:59:41 +07:00
pj c4cf0ff530 feat(driver): optional launcher_activity on Launch RPC for multi-alias apps 2026-04-18 01:59:36 +07:00
pj 6d81e24e71 feat(sidecar): real hierarchy via adb uiautomator dump 2026-04-18 01:24:04 +07:00
pj 447fd39363 feat(runner): fetch hierarchy per step and resolve Tap coords 2026-04-18 01:24:00 +07:00
pj 5f2a2d52ab feat(verifier): wire hierarchy into state.ax and carry element coords in Action 2026-04-18 01:23:55 +07:00
pj e539e89438 feat(hierarchy): XML parser and selector resolver for uiautomator dumps 2026-04-18 01:23:51 +07:00
pj e99c85c363 feat(cli): orchestrate full uatu test pipeline
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.
2026-04-18 00:51:44 +07:00
pj e7b3e2ba9c refactor(runner): caller manages app launch/terminate
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.
2026-04-18 00:51:39 +07:00
pj 0c68e72341 feat(sdk-android): publish to GitHub Packages at maven.pkg.github.com
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.
2026-04-18 00:16:33 +07:00
pj fad4f7db38 test(proto): include TapSelector in v0.1 RPC list 2026-04-18 00:09:27 +07:00
pj 4dbad9073e feat(spec): merchant-ledger property + login/navigation generators
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.
2026-04-18 00:08:40 +07:00
pj 7aa75f3d9f feat(bundler): add Aliases option for spec import resolution
Specs import from "@uatu/spec"; the bundler maps that to the
vendored pkg/spec-api/src/index.ts via esbuild's Alias map.
2026-04-18 00:08:31 +07:00
pj 8c6a3171d7 feat(cli): doctor check that surfaces placeholder sidecar JAR
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`.
2026-04-18 00:06:30 +07:00
pj 6a9fac7ec0 feat(sidecar): embed sidecar JAR via go:embed
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.
2026-04-18 00:06:27 +07:00
pj 69d0800ea3 feat(driver/maestro): gRPC client implementing driver.Driver
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.
2026-04-18 00:04:24 +07:00
pj f6efee1a9c feat(sidecar): wire tapSelector through DriverBackend + DriverService 2026-04-18 00:04:22 +07:00
pj b24463fc81 feat(proto): add TapSelector RPC and Selector message
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.
2026-04-18 00:04:20 +07:00
pj 6020a52adb test(sidecar): cover DriverService RPC delegation via in-process gRPC 2026-04-18 00:02:11 +07:00
pj fc209aaeb2 feat(sidecar): Main entry point binding the gRPC server to 127.0.0.1 2026-04-18 00:02:10 +07:00
pj 125a060a69 feat(sidecar): DriverService implementing v0.1 RPCs
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.
2026-04-18 00:02:08 +07:00
pj d9800b648f chore(sidecar): wire grpc + protobuf-gradle-plugin + maestro
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.
2026-04-18 00:02:06 +07:00
pj d856c40d7c feat(permissions): grant dangerous permissions via aapt + adb
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.
2026-04-17 23:57:17 +07:00
pj b6b18c3681 feat(cli): real doctor subcommand with adb/emulator/java checks
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.
2026-04-17 23:56:00 +07:00
pj 63796fbd42 chore(build): add Makefile with bootstrap, sidecar, sdk, uatu, test targets 2026-04-17 23:54:41 +07:00
pj d4a6e33aa6 feat(runner): pause-snapshot-evaluate-resume loop
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.
2026-04-17 23:54:05 +07:00
pj d2fae427f9 feat(driver): add TapSelector and make mock WaitForIdle honor duration
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.
2026-04-17 23:54:00 +07:00
pj abeded0b19 test(verifier): cover spec lifecycle and weighted action selection 2026-04-17 23:48:59 +07:00
pj 72175bb13c feat(verifier): goja runtime hosting the spec API
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.
2026-04-17 23:48:58 +07:00
pj 29c6ea4842 test(spec-api): cover runtime delegation for every public function 2026-04-17 23:45:37 +07:00
pj 4a1f8cb522 feat(spec-api): typed surface for spec authors
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.
2026-04-17 23:45:36 +07:00
pj 5988e93057 chore(spec-api): scaffold @uatu/spec package metadata
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.
2026-04-17 23:45:32 +07:00
pj 72c4e4c4d3 feat(trace): JSONL writer for steps + meta + screenshots
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.
2026-04-17 23:43:45 +07:00
pj 0c277b2833 feat(ltl): formula AST and step evaluator for v0.1
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.
2026-04-17 23:42:44 +07:00
pj cee90694da feat(bundler): esbuild wrapper for spec compilation
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.
2026-04-17 23:41:43 +07:00
pj 6353afdf83 feat(driver/mock): in-memory Driver for unit tests
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.
2026-04-17 23:40:28 +07:00
pj d533aef04b feat(driver): Driver interface for v0.1 RPC surface
Mirrors proto/driverpb/driver.proto: Launch, Terminate, Tap,
InputText, Hierarchy, Screenshot, WaitForIdle, Health. Keeps
Image/Health as dedicated structs so callers don't depend on
generated proto types.
2026-04-17 23:40:28 +07:00
pj 123cbf06e7 fix(sdk-android): post frame callbacks via main looper
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.
2026-04-17 23:39:11 +07:00
pj d991d99e0f feat: uatu-sample-harness for SDK round-trip validation
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.
2026-04-17 23:39:07 +07:00
pj 8961aadda8 feat(sdk-android): throwaway sample app for SDK validation
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.
2026-04-17 23:39:05 +07:00
pj 1700160077 feat(sdk-android): Uatu public API over SocketClient + Pauser
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.
2026-04-17 22:57:12 +07:00
pj 8560d32cfe chore(sdk-android): return default values for Android stubs in unit tests 2026-04-17 22:57:08 +07:00
pj 52944b1aa6 feat(sdk-android): Pauser with frame-callback-gated extractors
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.
2026-04-17 22:55:06 +07:00
pj ac49818283 feat(sdk-android): SocketClient with reconnect + LocalSocket transport
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.
2026-04-17 22:53:13 +07:00
pj b9511c0718 feat(agent): socket server with PAUSE/STATE/RESUME flow
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.
2026-04-17 22:50:54 +07:00
pj 2cc7617590 feat(sdk-android): Kotlin wire protocol matching the Go encoder
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.
2026-04-17 22:48:10 +07:00
pj ca92bb1492 feat(agent): Go wire protocol with length-prefixed JSON framing
Six message types: HELLO, PAUSE, RESUME, STATE, EXTRACT_RESULT,
GOODBYE. 4-byte big-endian length + JSON payload. 16MB frame cap.
Typed constructors per message; snapshots carry json.RawMessage so
downstream goja evaluation sees exact types.
2026-04-17 22:48:07 +07:00
pj 9cea3b66f0 feat(sdk-android): scaffold Android library module
AGP 8.11.0, Kotlin 2.1.21, minSdk 24, compileSdk 35, JDK 17
bytecode, no Compose dep. Uatu object exposes start/extract as
stubs; real implementation in tasks #6–8.
2026-04-17 22:38:04 +07:00
pj 9a792e20ce feat(sidecar): scaffold Kotlin JVM module with Shadow fat JAR
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.
2026-04-17 22:38:01 +07:00
pj b594108595 chore(gradle): add root settings for uatu multi-project build
Links :sidecar and :sdk-android under one wrapper. Root build.gradle
provides a clean task that wipes the root build dir.
2026-04-17 22:37:53 +07:00
pj ac80114b2d chore(gradle): add Gradle wrapper 9.4.1
Committed so the repo builds reproducibly without a host gradle.
2026-04-17 22:37:51 +07:00
pj 32cd729f9d chore: add .gitignore for gradle, IDE, and run outputs 2026-04-17 22:37:46 +07:00
pj 1c4b0dd00f feat(cli): scaffold uatu CLI with test and doctor subcommands
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.
2026-04-17 22:29:09 +07:00
pj d73192b235 test(proto): cover driver service surface
Asserts the eight v0.1 RPCs are exactly the ones present and
that message fields round-trip through generated getters.
2026-04-17 22:27:32 +07:00
pj 7c988b53dd chore(proto): generate Go stubs for driver service
Brings in google.golang.org/grpc and protobuf dependencies.
2026-04-17 22:27:31 +07:00
pj 9d62ba963d feat(proto): define driver v0.1 service contract
Eight RPCs needed for v0.1 per plan: Launch, Terminate, Tap,
InputText, Screenshot, Hierarchy, WaitForIdle, Health.
Defers Swipe/LongPress/OpenLink/etc until a spec needs them.
2026-04-17 22:27:27 +07:00
pj ee78371566 chore(proto): add buf config for codegen and lint
Relaxes STANDARD lint rules that conflict with the terse service
shape from ARCHITECTURE.md §7.2 (shared Empty, no Service suffix).
2026-04-17 22:26:23 +07:00
pj 8d5b32c10b init project with readme 2026-04-17 22:07:09 +07:00