From e6eca1d51185807b81c43ab20985e35472827c24 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 11 Oct 2026 08:46:37 -0600 Subject: [PATCH] Makes the live datamodel pane follow the scrubber The live inspector's scrubber moved the diagram, the note and the event log to a selected macrostep, while its datamodel pane kept folding the whole stream. The updater now hands the pane the same selection the other panes read, so on a selected macrostep it shows the values as they stood at the end of that macrostep, and Live returns it to the current values - as the persisted stepper already did. A test drives the updater over a chart whose datamodel moves and reads the rendered pane at First and at Live. The notebook's scrubber step and the Kino moduledoc say the pane follows; a Changed fragment carries the host-facing line. Refs: sui-25h --- changelog.d/sui-25h.md | 5 +++ lib/statifier_ui/kino.ex | 13 +++--- notebooks/inspector.livemd | 15 ++++--- test/statifier_ui/kino_test.exs | 71 +++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 changelog.d/sui-25h.md diff --git a/changelog.d/sui-25h.md b/changelog.d/sui-25h.md new file mode 100644 index 0000000..58fb173 --- /dev/null +++ b/changelog.d/sui-25h.md @@ -0,0 +1,5 @@ +### Changed + +- The live inspector's datamodel pane (`StatifierUI.Kino.inspect/3`) follows + its scrubber: on a selected macrostep it shows the values as they stood at + the end of that macrostep, and **Live** returns it to the current values. diff --git a/lib/statifier_ui/kino.ex b/lib/statifier_ui/kino.ex index 06ad93f..bac7fa7 100644 --- a/lib/statifier_ui/kino.ex +++ b/lib/statifier_ui/kino.ex @@ -13,11 +13,12 @@ if Code.ensure_loaded?(Kino) do ## The scrubber (sui-3gg) Four buttons above the diagram - **First**, **Prev**, **Next**, - **Live** - move the diagram from the live tip to any macrostep in the - log and back. Selecting a macrostep draws the configuration that - macrostep settled in, opens its entry in the event log, and marks that - entry `- selected`; the note above the diagram says which - point is on screen. `StatifierUI.Inspector` decides all of it, so the + **Live** - move the diagram and the datamodel explorer from the live + tip to any macrostep in the log and back. Selecting a macrostep draws + the configuration that macrostep settled in, shows the datamodel as it + stood at the end of that macrostep, opens its entry in the event log, + and marks that entry `- selected`; the note above the diagram says + which point is on screen. `StatifierUI.Inspector` decides all of it, so the whole behaviour is testable without Kino - this module only wires buttons to `Updater.scrub/2`. @@ -512,7 +513,7 @@ if Code.ensure_loaded?(Kino) do Kino.Frame.render( state.frames.datamodel, - Kino.Markdown.new(Inspector.datamodel(messages)) + Kino.Markdown.new(Inspector.datamodel(messages, opts)) ) Kino.Frame.render(state.frames.log, Kino.Markdown.new(Inspector.event_log(messages, opts))) diff --git a/notebooks/inspector.livemd b/notebooks/inspector.livemd index 14b5d92..d5e5667 100644 --- a/notebooks/inspector.livemd +++ b/notebooks/inspector.livemd @@ -169,13 +169,12 @@ caught-up stream got there through statifier ADR-0034 replay. (`sui-dc7`), and the note keeps the two readings apart: a configuration the chart *exited* in is never worded as one it *settled* in. -The datamodel explorer in **this** widget does not move with the -scrubber - it always shows the current values. The per-macrostep fold -exists (`StatifierUI.Inspector.datamodel/2` takes the same `:selection` -every other read here does, `sui-2uz`), and the persisted stepper in step -10 is built on it; wiring it into the live inspector's own pane is a -separate change. Here the diagram and the log are the pair this walk -exercises. +The datamodel explorer follows the scrubber too. On a selected macrostep +it shows the values as they stood at the end of that macrostep - the fold +`StatifierUI.Inspector.datamodel/2` takes under the same `:selection` +every other read here does, and the one the persisted stepper in step 10 +shows. Back at **|< First**, expect `authorization_attempts` and +`captured_cents` at `0`; **Live** returns the pane to the current values. ## 7. The form, including its error path @@ -243,7 +242,7 @@ number and event. 1. Pick a macrostep from **Jump to**. Expect: the note, the diagram, the event log's marked entry, **and the datamodel pane** all move to that - point together - unlike step 6, the values shown are the ones that + point together - as in step 6, the values shown are the ones that macrostep ended with, not the ones the run finished with. 2. Read the **Datamodel changes in macrostep N** table below the panes. Expect: one row per slot the step moved, with the value before and diff --git a/test/statifier_ui/kino_test.exs b/test/statifier_ui/kino_test.exs index 6c9f6e6..f3ac9ca 100644 --- a/test/statifier_ui/kino_test.exs +++ b/test/statifier_ui/kino_test.exs @@ -140,6 +140,57 @@ defmodule StatifierUI.KinoTest do end end + describe "the scrubber's updater over a datamodel that moves" do + setup do + machine = SessionCase.compile!(@counter) + {sub, session} = SessionCase.start_early!(machine, "sess_kino_scrub_datamodel") + Session.send_event(session, "bump") + SessionCase.wait_for_macrostep(session, 2) + Session.send_event(session, "bump") + SessionCase.wait_for_macrostep(session, 3) + wait_for_buffered_macrostep(sub, 3) + + frames = + Map.new([:status, :note, :diagram, :datamodel, :log], fn key -> + {key, Kino.Frame.new(placeholder: false)} + end) + + {:ok, updater} = + Updater.start_link( + sub: sub, + machine: machine, + frames: frames, + initial_configuration: [0, 1] + ) + + %{updater: updater, frames: frames, messages: Subscriber.messages(sub)} + end + + # Sabotage: rendered the live datamodel pane with `Inspector.datamodel/1` + # (no selection) in `Updater` - this test went red on the scrubbed + # assert, showing the live tip's count where the first macrostep's was + # expected, and the other updater tests stayed green. + test "the datamodel pane follows the scrubber and returns to the tip on live", + %{updater: updater, frames: frames, messages: messages} do + at_first = Inspector.datamodel(messages, selection: {:macrostep, 1}) + at_tip = Inspector.datamodel(messages) + + # The fold differs between the two points, so the pane can only match + # one of them. + assert at_first =~ "42" + assert at_tip =~ "44" + refute at_first == at_tip + + :ok = Updater.scrub(updater, :first) + _settled = :sys.get_state(updater) + assert rendered_text(frames.datamodel) == at_first + + :ok = Updater.scrub(updater, :live) + _settled = :sys.get_state(updater) + assert rendered_text(frames.datamodel) == at_tip + end + end + describe "truth_table/2" do setup do {:ok, fixtures} = @@ -383,4 +434,24 @@ defmodule StatifierUI.KinoTest do assert Process.alive?(stepper) end end + + # The Markdown text a frame holds now - what the widget shows - read + # through `Kino.Frame.get_items/1`, the frame's own record of its content. + defp rendered_text(frame) do + [%Kino.Markdown{text: text}] = Kino.Frame.get_items(frame) + text + end + + # The session reports a macrostep before its subscriber has necessarily + # buffered every message of it; poll the buffer itself. + defp wait_for_buffered_macrostep(sub, target, timeout \\ 1000) do + buffered? = Enum.any?(Subscriber.messages(sub), &(&1.macrostep == target)) + + if buffered? or timeout <= 0 do + :ok + else + Process.sleep(10) + wait_for_buffered_macrostep(sub, target, timeout - 10) + end + end end