Files
sanderling/docs/development/architecture.md
T
pj 88db0cbea8 docs: web platform + clean URLs + dark/light mode (#37)
* chore(docs): replace d2 diagram pipeline with mermaid

Remove docs/_diagrams/ and d2 build step from Makefile. The HTML
template already initialises mermaid.js; diagrams are now inline
code fences rendered client-side.

* docs(architecture): add mermaid diagram + web/CDP platform docs

Replace SVG img tag with inline mermaid flowchart showing both native
(Maestro sidecar + in-app SDK) and web (Chrome CDP) paths. Update
Processes, Transports table, and per-step cycle sections.

* docs(design-principles): update principles 1-4 for web platform

Principles 1, 2, 3, and 4 referenced Maestro and native-only concepts.
Add web/CDP context and update driver-is-an-interface to name both
sidecar and chrome implementations.

* docs(manual): add web prerequisites and folio-web example

Update --platform flag to list android, ios, web. Add web prerequisites
section (Chrome, no SDK needed) and folio-web quick-start to
getting-started.

* chore(gitignore): untrack inspect dist/index.html build artifact

index.html is regenerated by vite on every build with a new content hash,
making it permanently dirty. Only .gitkeep is needed for //go:embed to
compile on a fresh checkout. Also remove duplicate dist/* line and stale
d2 diagram ignore entries.

* feat(docs): click-to-zoom for mermaid diagrams

* docs(architecture): change diagram layout from LR to TB

* docs(getting-started): link npm and Maven Central package headers

* update docs root

* docs(spec): rewrite npm package README

Update usage example to current API, drop stale version-compatibility
and license sections.

* build(docs): output pages as pagename/index.html for clean URLs

Split DOCS_OUT into INDEX_OUT (index.md files stay as index.html) and
PAGE_OUT (all other pages become pagename/index.html). The __ROOT__
depth computation already handles the extra directory level correctly.

* chore(docs): update sidebar links to directory-style URLs

* docs: update cross-links from .html to directory-style paths

* ci(docs): remove d2 install step

* feat(docs): dark/light mode toggle

Add theme toggle button (top-right, fixed). Persists preference in
localStorage; falls back to prefers-color-scheme. Flash-free via inline
script in <head> that sets data-theme before first paint.

* fix(docs): fix inspect image path broken by directory URL restructure

* feat(docs): click-to-fullscreen for all article images

* fix(inspect): allow AssetsFS override in ServerOptions; drop unused request param from serveIndex

* fix(inspect): use in-memory FS in tests so TestAssets_FallbackToIndexHTML passes without web build
2026-04-23 00:57:30 +07:00

3.5 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["Maestro sidecar (JVM)"]
    SDK["in-app SDK\n(Device / Emulator)"]
    CH["Chrome (CDP)"]
    RD[("runs/")]
    IN["sanderling inspect\nHTTP + SSE"]
    UI["Web UI (React)"]

    D -->|gRPC| SC
    SC -->|UIAutomator / XCTest| SDK
    R -->|"Unix socket<br/>(pause / state / logs)"| SDK
    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.

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

In-app SDK. A Kotlin (or Swift for iOS) library linked into the app under test. Exposes a Unix socket to the runner. Provides pause and resume, view-hierarchy dumps, coverage reads, log capture, and user-registered state extractors. Native platforms only.

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

Transports

Channel Platform Transport Purpose
Go to Maestro sidecar Native gRPC (localhost TCP) UI input, screenshots, system alerts
Go to in-app SDK Native Unix domain socket Pause / resume, hierarchy, coverage, logs, extractors
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:

pause  ─►  capture state  ─►  evaluate properties  ─►  pick action  ─►  resume  ─►  dispatch

Native (Android / iOS):

  1. The runner asks the driver to wait until the UI is idle.
  2. The runner sends PAUSE to the SDK over the Unix socket. The SDK freezes the main runloop at a safe point.
  3. The SDK sends back a STATE message: view hierarchy, coverage delta, logs since last step, exception list, snapshot values.
  4. The runner feeds state into goja. Extractors re-read; properties re-evaluate; the action generator returns a weighted tree.
  5. The runner writes the trace entry for this step.
  6. The runner picks an action by weight.
  7. The runner sends RESUME to the SDK, then dispatches the action through the driver (gRPC to sidecar → Maestro → UIAutomator or XCTest).
  8. Loop.

Web (Chrome):

Steps 2-3 use CDP to capture the DOM hierarchy and console logs directly; there is no SDK pause/resume. 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.