Commit Graph
11 Commits
Author SHA1 Message Date
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 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 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 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