Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-robotui/Cargo.toml

[package]
name = "nornir-robotui"
version = "0.4.0"
edition = "2024"
rust-version = "1.85"
description = "Reusable robot-UI testing framework for egui apps: drive real interactions (click/scroll/type/key) via egui_kittest (AccessKit), observe via each component's state_json (the nordisk Observable pattern), and emit each (action → expected outcome) step as a nornir-testmatrix row — so a robot run auto-clicks around and FILLS the test matrix. Reusable by korp, facett-demo, mannequin, and nornir's own viz."
license = "MIT OR Apache-2.0"
repository = "https://git.gunnar.rs/nordisk/edda"
readme = "README.md"
keywords = ["egui", "testing", "ui", "robot", "matrix"]
categories = ["development-tools::testing", "gui"]
# Keep the published crate lean: the .nornir design/runtime dir is not part of
# the library.
exclude = ["/.nornir/"]

[features]
# Core is light: drive + observe + emit matrix rows, no GPU. The `snapshot`
# feature pulls egui_kittest's wgpu renderer so `RobotSession::snapshot(name)`
# can render real PNG proof — opt-in so a headless CI run that only asserts
# state_json doesn't drag in the wgpu stack.
default = []
# The render-oracle snapshot harness: wgpu render → lossless WebP → warehouse
# diff. Pulls `image` (raw pixels), `webp` (libwebp lossless encode/decode), and
# `sha2` (content-address dedup). Opt-in so a headless state_json-only CI run
# doesn't drag in the wgpu/webp stack.
snapshot = [
    "egui_kittest/wgpu",
    "egui_kittest/snapshot",
    "dep:image",
    "dep:webp",
    "dep:nornir-hash",
]
# The live WATCH backend: replay the SAME `Scenario` in a REAL, visible eframe
# window on your display — the robot moves a cursor and clicks/types/scrolls
# through it, paced so a human can watch — instead of the headless off-screen
# kittest drive. Watch is ADDITIVE: `RunMode::Headless` (the default) still
# emits `TestResultRow`s for CI; `RunMode::Watch` opens the window. Opt-in and
# feature-gated so the lean matrix harness (korp / facett-demo / mannequin) never
# pulls the eframe/winit/**wgpu** windowing stack. It also turns on egui's `serde`
# (so a `Scenario` round-trips through TOML — the `robotui watch <scenario.toml>`
# CLI) and, via the `robotui` bin's `required-features`, builds the bin.
#
# `glow` is NOT in that stack and has not been since 2026-08-01 (LAW #4, and see the
# `eframe` dependency line below): a `watch` build gets WebGPU, full stop. This
# comment said "eframe/winit/glow" until 2026-08-24, which was a build recipe telling
# the reader the opposite of what the manifest resolves.
watch = ["dep:eframe", "dep:serde", "dep:toml", "dep:image", "egui/serde", "wire"]
# **A NO-OP ALIAS of `watch`, kept only for the consumers that name it.** It adds
# `eframe/wgpu`, which the `eframe` dependency line below already carries
# unconditionally, so this feature cannot change a single byte of the build. Enabling
# it and enabling plain `watch` resolve to the identical dependency graph.
#
# It used to be real: the eframe line read `glow`, and this feature was the difference
# between a WebGPU watch window and an OpenGL one. LAW #4 put `wgpu` on that line for
# every build and deleted `WatchRenderer::Glow`, which made the switch redundant — but
# only the manifest was fixed, and this comment went on describing the renderer choice
# for eight days. That is worse than a stale sentence: a flag that silently does
# nothing reads on the receipt as a knife that was used.
#
# NOT DELETED because deleting it is a cross-repo change, not a leaf one: korp
# (`"nornir-robotui/watch-wgpu"`) and facett-demo (`features = ["watch-wgpu"]`) name
# it, and korp's `tests/gpu_status_is_wired.rs` asserts this manifest still declares
# it. Retiring it is those repos' call, in their commit.
#
# `tests/watch_wgpu_is_an_alias.rs` reads this manifest and RED-FAILS if the algebra
# ever stops matching the paragraph above — if `wgpu` leaves the eframe line, if this
# feature grows a real effect, or if any source file cfg-gates on it (such a gate
# compiles exactly when `watch` does, so it reads as a choice and is not one).
watch-wgpu = ["watch", "eframe/wgpu"]
# The `cmd_json` WIRE channel (module `cmd_json`): a serde command/assertion
# protocol + host + client so a driver drives a REMOTE egui process the same way
# `RobotSession` drives a co-located one. It only needs serde on the SHARED
# `Action`/`Assert`/`Target`/`Step` vocabulary (and `egui/serde` for the
# `egui::Key`/`Vec2`/`Pos2`/`Event` those carry) — NO eframe/windowing. Opt-in so
# the lean headless matrix build never (de)serializes the command set. `watch`
# turns it on transitively (it already enables the same serde), so a `watch` build
# gets the wire channel for free.
wire = ["dep:serde", "egui/serde"]
# ── `xsession` — REMOVED 2026-08-07, X is not a supported robot-UI backend ──
# The `xsession` feature, its `robot-xsession` bin and the 2,387 lines behind them are
# gone. It was the rank-5 real-window leg built on a private Xvfb display driven through
# Xvfb / xdotool / xwininfo / import.
#
# Robot UI runs on **Wayland only** now, so an X-only harness had no world left to run in.
# Its rank-5 job — a real window, a real surface, a real adapter — is not dropped: the
# `watch` backend opens a real window and captures the window's own presented framebuffer
# (`display::write_rgba_png`), which needs no compositor permission and so works inside the
# nested Wayland compositor a drive should use to stay off the operator's screen.
#
# The four 2026-08-03 phantom findings it was built to prevent were all consequences of the
# X leg itself (no WM to focus a window, two instances on one display, a `/proc` scan to
# tell Xvfb from X11, llvmpipe mistaken for an RTX 4090). Deleting the backend deletes the
# class rather than keeping three post-conditions alive to police it.


[dependencies]
# Drive: real AccessKit-backed egui interactions (click/type/key/scroll/drag),
# frame stepping, and (behind `snapshot`) the wgpu renderer for PNG proof.
egui_kittest = { version = "0.36", default-features = false }
egui = "0.36"
# Emit: each scripted step becomes a TestResultRow so a robot run fills the
# nornir test matrix. The ONLY heavy-ish dep, and it is itself lean (std+serde).
nornir-testmatrix = { version = "0.2.7", path = "../nornir-testmatrix" }
# Replay: the saga record/replay engine's toolkit-neutral `ReplayPlan`/
# `ReplayStep`. The `replay` module bridges a recorded session's steps into a
# robot `Scenario`, so a captured user journey drives the real egui app and
# fills the matrix. Serde-only core (no capture buffer pulled — `default = []`).
nornir-saga = { version = "0.2.0", path = "../nornir-saga" }
# Observe: the app's introspection blob is serde_json::Value.
serde_json = "1"
anyhow = "1"
# Only used to save the rendered PNG / raw pixels under `snapshot`.
image = { version = "0.25", optional = true }
# Lossless WebP encode/decode for the snapshot reference store (libwebp). The
# `image` 0.25 native WebP encoder is decode-biased, so we use libwebp directly.
webp = { version = "0.3", optional = true }
# Content-address dedup hash for stored references (`snapshot` only) — the ONE
# shared hash leaf (`sha256_hex`), consumed not re-invented.
nornir-hash = { version = "0.1.0", path = "../nornir-hash", optional = true }
# The live WATCH backend (feature `watch`) mounts the app in a REAL eframe
# window. `accesskit` keeps the live tree available; optional so the lean/CI build
# pulls no windowing stack.
#
# LAW #4 — the backend is `wgpu`, NEVER `glow`. This line used to read `glow`, with
# the comment "glow (not wgpu) matches the workspace's existing eframe pin". That
# justification was already false when written and is doubly so now: the fleet is
# WebGPU everywhere, and eframe resolves `Renderer::default()` to `Glow` whenever its
# `glow` feature is on — so the live watch window rendered on OpenGL, decided by one
# word in a manifest with nothing in the source saying so. Same defect as
# knut-popviz (knut 37feaa6) and knut-pipelines (knut 46c6c83).
#
# `watch-wgpu` ABOVE is a no-op alias of `watch` because of this line: it adds
# `eframe/wgpu`, which this line already carries unconditionally. The two facts are
# one fact, and `tests/watch_wgpu_is_an_alias.rs` is what keeps them from drifting.
#
# `x11` is DELIBERATELY ABSENT (2026-08-07). Robot UI is Wayland-only, and
# `watch::run` refuses a non-Wayland backend at run time — but a runtime check is a
# guard, and NOT compiling the X11 backend is the fix by construction (L5). With
# `x11` on, winit can still fall back to an X connection at bring-up, which is
# exactly the path that let a drive believed to be headless open a window on the
# operator's real screen. Take the capability away and the fallback cannot happen.
eframe = { version = "0.36", default-features = false, features = ["default_fonts", "wgpu", "wayland", "accesskit"], optional = true }
# Serde derive on the `Scenario` types + TOML load for the `robotui` CLI. Both
# are `watch`-only: the lean matrix harness never (de)serializes scenarios.
serde = { version = "1", features = ["derive"], optional = true }
toml = { version = "1", optional = true }

# The `robotui` CLI: `robotui run <scenario.toml>` (headless matrix) and
# `robotui watch <scenario.toml>` (live paced window). Behind `required-features`
# so a plain `cargo build` of the lean lib never compiles the bin or eframe.
[[bin]]
name = "robotui"
path = "src/bin/robotui.rs"
required-features = ["watch"]

[dev-dependencies]
# The reference test drives a tiny self-contained egui app; the viz reference
# test lives in the nornir root crate (it needs the heavy viz deps).
egui_kittest = { version = "0.36", default-features = false }
# The snapshot harness tests write a `.actual.webp` to an isolated temp dir.
tempfile = "3"
# `tests/watch_wgpu_is_an_alias.rs` reads THIS manifest and asserts the feature
# algebra its comments claim (the `watch-wgpu` alias, no `glow`, no `x11`) — so the
# manifest and the prose beside it cannot drift apart again silently. Already an
# optional dep of the `watch` feature, so it costs the lockfile nothing.
toml = "1"