Files
sanderling/docs/development/architecture.md
pj f572c8ba66 WIP: docs: refresh after iOS + web support (#50)
* docs: README covers iOS + web, surface both example apps

* docs(cli): document --ios-device and per-platform doctor

* docs: tighten README, fold examples into Docs list

* docs(runs): correct --clear-data lifecycle wording

Default behavior no longer wipes app data between runs; --clear-data is now opt-in.

* docs(getting-started): add iOS path, separate folio and folio-web

Document just test-ios under examples/folio, and distinguish the KMP
sample from the React + Vite folio-web sample.

* docs(inspect): document the eight panels

Lists Screenshot, ActionList, Timeline, ViolationsPanel, HierarchyPanel,
SnapshotTable, MetricsChart, ExceptionsPanel. Cross-links HierarchyPanel
to the spec language reference.

* docs(writing-specs): document setup export, flag noLogcatErrors as android-only

Mirrors pkg/spec/README.md so the manual covers the runner's setup-first
fall-through. Marks noLogcatErrors as Android-only so iOS/web spec
authors know it silently no-ops.

* docs(folio): document web target and iOS sanderling test recipe

After the KMP refactor folio also runs on wasmJs and the justfile exposes
just web, just web-build, and just test-ios. Surface all three.

* docs(folio-web): add README

Covers prerequisites, demo credentials, just test recipe, and how the
React + Vite host exposes state to the sanderling spec via stable ids
and data-* attributes.

* docs: scrub driver-implementation name from user docs

Drop the implementation tool name from README, cli.md doctor table, and
spec-language.md. These docs should describe behaviour, not the specific
underlying tool the native sidecar wraps.

* docs(development): scrub driver-implementation name from dev docs

architecture, design-principles, decisions now describe the native
sidecar by role (gRPC surface over OS UI-test pipeline) rather than by
the specific tool it wraps.
2026-05-25 16:17:21 +05:30

2.7 KiB

title
title
Architecture

Architecture

flowchart TB
    subgraph go["sanderling (Go)"]
        direction LR
        B["Bundler / esbuild"] --> V["Verifier / goja + LTL"]
        V <--> R["Runner"]
        R --> D["DeviceDriver"]
        R --> T["Trace writer\nJSONL + PNG"]
    end

    SC["Native sidecar (JVM)"]
    DC["Device / Emulator"]
    CH["Chrome (CDP)"]
    RD[("runs/")]
    IN["sanderling inspect\nHTTP + SSE"]
    UI["Web UI (React)"]

    D -->|gRPC| SC
    SC -->|UIAutomator / XCTest| DC
    D -->|CDP| CH

    T --> RD --> IN --> UI

Processes

sanderling (Go). The top-level binary. Bundles the spec with esbuild, evaluates it in goja, runs the main loop, dispatches actions through the DeviceDriver interface, writes the trace.

Native sidecar (JVM). A Kotlin process that exposes a gRPC surface matching the DeviceDriver interface. Handles UI input, screenshots, the system accessibility tree, and OS-level alerts. Native platforms only.

Chrome (CDP). For web targets, the Go binary drives Chrome directly over the Chrome DevTools Protocol. No sidecar is involved.

Transports

Channel Platform Transport Purpose
Go to native sidecar Native gRPC (localhost TCP) UI input, screenshots, system alerts
Go to Chrome Web Chrome DevTools Protocol UI input, screenshots, DOM hierarchy, console logs

On native, the transport split exists because only real UI events need to cross process and OS-API boundaries. Introspection is cheap, frequent, and lives on a fast local socket directly to the app. On web, CDP handles both.

Inspect UI

sanderling inspect is a separate mode of the same Go binary. It serves an embedded React bundle and reads runs/ from disk, streaming file-watcher events over SSE so the UI updates as new steps land. It has no connection to any driver; it only consumes the trace artifacts.

Per-step cycle

The heart of the system is:

fetch state  ─►  evaluate properties  ─►  pick action  ─►  dispatch

Native (Android / iOS):

  1. The runner asks the driver to wait until the UI is idle.
  2. The runner fetches the UI hierarchy and logs from the sidecar.
  3. The runner feeds state into goja. Extractors re-read; properties re-evaluate; the action generator returns a weighted tree.
  4. The runner writes the trace entry for this step.
  5. The runner picks an action by weight and dispatches it through the driver (gRPC to sidecar -> UIAutomator or XCTest).
  6. Loop.

Web (Chrome):

CDP captures the DOM hierarchy and console logs directly. The rest of the cycle is identical.

The cycle runs hundreds of times per minute. Every step produces one row in trace.jsonl and one screenshot.