Saga — design
nornir-saga: capture a user's navigation as structured events → persist to the nornir warehouse → the test matrix replays the path as a generated robot test for bug-finding + regression. The production sibling ofnornir-testmatrix.
Lineage — two siblings, one pattern
emit real state from inside → nornir warehouse → read back.
nornir-testmatrix |
nornir-saga |
|
| Emits | test verdicts (functional_status) |
navigation + log events (record(SagaEvent)) |
| Gate | feature testmatrix (dev/test, release-stripped) |
feature saga (production opt-in) |
| Sink | nornir warehouse test_results |
nornir warehouse saga_events |
| Read back | the matrix (green ⟺ emitted ok) | matrix replays the path → robot test |
Pieces
SagaEvent+EventKind(event.rs) — one toolkit-neutral row of the journey:seq · ts_micros · session_id · user · kind[nav|click|log|error|state] · route · component · action · detail · state_json · level · span_id · parent · app · app_version · git_sha. Navigation is the primary event. Builders (nav/click/log/error/state) +SessionCtx/in_session(the adapter stamps session/app identity).- Capture buffer (
record.rs) —record(event)/drain_saga_events(), gated behindsaga. ON = stampseq+ts, PII-redact by default, and buffer; OFF =#[inline]no-op, hard-empty drain. Same release-gating discipline as testmatrix. - Untrusted-client ingest (
record.rs) —ingest(events, &ctx): the reusable, toolkit-neutral half of a web app's client→server transport. A browser (WASM) captures a journey and POSTs the rawVec<SagaEvent>to a server endpoint; the app owns only the framework-specific wire (#[server]/ Axum handler) and hands the decoded batch here. Each event is re-stamped with the server-derivedSessionCtx(a client cannot forge another user's identity) andrecorded — so it shares the same PII-redaction and the same flush → warehouse pipe. The batch is capped atMAX_INGEST_BATCHto bound an untrusted caller's memory, and the recorded count is returned so a truncated over-cap batch is logged, never silently dropped. SagaSinktrait (sink.rs) — the warehouse seam: one method,write(&[SagaEvent]) -> Result<()>. Saga shipsNullSink(drop) andJsonFileSink(append +read_all); nornir implementsIcebergSagaSinkagainst the nornir warehousesaga_events. Saga itself stays lean (serde only) and never names a backend.- Replay plan (
replay.rs) — orderedNav/Clickevents → toolkit-neutralReplaySteps the matrix turns into a robot test (ReplayPlan::for_session/all_sessions).
Toolkit-neutral core + adapters (the deferred "emitters")
The core has no UI/framework deps. Capture happens through thin adapters that build SagaEvents and call record:
- egui adapter — at the facett
nav/Navigableboundary. - web app (Leptos + Axum) adapter — Leptos router events (client) + an Axum middleware layer (server).
Replay drivers mirror that split: egui → kittest / nornir-robotui, web app → a WebDriver driver against the live Leptos app. Adapters + drivers are the next phase; the core lands first.
The warehouse is always the nornir warehouse
Saga writes to the nornir warehouse — not a per-feature sink choice, no alternative backend at the saga layer. How the warehouse stores (or is later detached behind a trait) is a nornir concern, not saga's. See plan item WAREHOUSE-TRAIT.
Status
Foundation landed (task #25): event.rs + record.rs (capture buffer, saga feature) + sink.rs (SagaSink trait + JsonFileSink/NullSink) + replay.rs (ReplayPlan/ReplayStep, Nav/Click → steps) + lib.rs; the crate is a workspace member (nornir-saga = { path = … }, pulled WITHOUT the saga capture feature so release pays zero cost).
-
Warehouse:
saga_eventsIceberg table (session_id, seq, ts_micros, kind, route, component, action, detail, state_json (PII-free), level, span_id, parent, app_user, app, app_version, git_sha, sha), partitioned bysession_id. nornir implementsSagaSinkover it (IcebergSagaSink) insrc/warehouse/saga_events.rs(+query_saga_events/list_saga_sessions/session_replay_plan). -
Replay drivers (the design split):
- egui → kittest / nornir-robotui — WIRED end-to-end:
nornir_robotui:: replay::plan_to_scenario(plan)turns aReplayPlaninto a robotScenariotheRobotSessiondrives; a vanished widget is a RED matrix row. - web → WebDriver / fantoccini — SCAFFOLDED against
src/robot.rs:replay_plan_to_web_scenario(plan)mapsNavigate→Goto+AssertUrlContains,Activate→ClickRole, ready forrun_scenariosagainst a live Leptos app.
- egui → kittest / nornir-robotui — WIRED end-to-end:
-
Viz: a 🎬 Saga Sessions pane under the 🧪 Test tab (
src/viz/test_tab.rs) lists recorded sessions (lock-tolerantopen_read_only), replays-on-click, and foldsstate_json["test"] ["saga_sessions"].
Deferred to phase 2: the capture adapters that emit saga_events from inside the apps — egui at the facett nav/Navigable boundary; Njord (Leptos router events client-side + an Axum middleware layer server-side). The web replay driver becomes live once those adapters record sessions. See nordisk_plan.md → SAGA.