Raxol.Harness.Surface.Golden (Raxol v2.6.1)

View Source

Byte-golden snapshot matrix for the harness degradation ladder: renders each fixed fixture session, end-to-end, through Raxol.Harness.Surface in each of the three render modes (:inline_log, :tmux_conservative, :flat) at fixed geometry, and blesses/checks the raw emitted byte stream against a checked-in golden file per fixture x mode pair.

Mirrors the RATE --gen / Raxol.Harness.Fixture.Bless precedent one layer further down the stack: RATE hashes rendered frames, Fixture.Bless snapshots the journal-fold projection, and this module snapshots the raw bytes a real terminal would receive -- the level below both, where a degradation-tier regression (a stray \e[2J, a footer bleed, a changed cursor dance) shows up as a literal byte diff rather than a semantic one.

Determinism audit

Every render in this matrix must be byte-identical run over run, VM over VM, machine over machine -- otherwise "golden" is a lie. Five potential sources of nondeterminism, and why each is closed:

  1. Time. render/2 drives Surface.advance/2 in a loop and NEVER passes a now (see render/2 below -- the loop always calls Surface.advance(model), arity 1, defaulting now to nil). The status strip's elapsed-ticker values derive solely from fixture event ts fields, never a live clock: Raxol.Harness.StatusStrip's own moduledoc ("The elapsed ticker and R11 (no wall-clock in the default suite)") documents that render/2 never calls System.monotonic_time/1 or any wall-clock function -- elapsed is a pure subtraction of two caller-supplied integers. StallDetector's notion of time is likewise caller-owned and is never fed into this render path at all (this module never touches StallDetector).

  2. Environment. An explicit :mode option to Surface.new/2 bypasses ModeSelect.select_with_reason/3 entirely (see that function's own "test seam" doc) -- no System.get_env/0 snapshot, no tty/CI detection, and no startup mode notice varies run to run. render/2 always passes an explicit :mode and env: %{} anyway, as hygiene: even though the explicit mode makes env dead for mode-pick purposes, passing a fixed empty map rather than System.get_env/0 means no accidental future code path in Surface.new/2 could reach into a live, machine-dependent environment map.

  3. Capabilities. render/2 always passes capabilities: nil explicitly. nil means neither Raxol.Terminal.Capabilities.cached/0 (a persistent_term-backed, machine-dependent probe) nor any $TERM sniffing ever reaches the byte stream -- every render in this matrix is built from the same, fully-specified capability record (none). nil is also exactly the conservative clamp :tmux_conservative itself assumes (see ModeSelect's moduledoc: the capability ladder clamps for a detected multiplexer before the record ever reaches InlineAuthority.new/5), so :tmux_conservative's golden renders with precisely the capabilities the real ladder would hand it for an un-probed/conservative terminal.

  4. Geometry. Fixed at [width: 60, rows: 20, footer_rows: 6] for every fixture x mode pair -- the exact geometry test/harness/t13a_surface_test.exs already uses for its own end-to-end assertions, so this matrix's byte streams are directly comparable to that suite's documented behavior.

  5. Map iteration order. This class is closed by the runtime's own determinism, not by any tripwire in this module: identical Erlang/ Elixir maps iterate in identical order within a VM -- map iteration order is a deterministic, platform-stable function of the keys alone (sorted order for small maps, a fixed-hash HAMT layout above 32 entries) -- so two renders of the same fixture in the same process can never diverge on map-iteration order by itself. What the DETERMINISM test in test/harness/golden_snapshot_test.exs (rendering the same fixture x mode pair TWICE in one test and asserting byte equality) actually catches is unseeded randomness, process-dictionary or persistent_term state leaking between calls, and clock leakage -- anything whose value can differ between two calls in the SAME VM run. The cross-machine golden comparison (the checked-in golden file, compared byte-for-byte across CI runs on different machines/VM instances) is the backstop for anything environment-shaped that a single-VM determinism test cannot see at all.

:tmux_conservative vs :inline_log

Per ModeSelect's own moduledoc, there is no separate TmuxConservativeAuthority -- :tmux_conservative routes through the exact same InlineAuthority as :inline_log (the reflow seam is detection-only today). So the :tmux_conservative golden for a given fixture MAY be byte-identical to that fixture's :inline_log golden, today. Each tier still gets its own pinned golden file rather than being aliased or skipped, so that the day a real rendering difference gates on tier (a transient-region algorithm, a capability clamp that changes emitted bytes), the two tiers can diverge independently without this matrix needing to be restructured.

Bless status conventions

run/1's bless path (check: false) deliberately extends Raxol.Harness.Fixture.Bless's :written/:current/:drift/:skipped status convention rather than reusing it verbatim: :written is split into :created (no golden existed on disk yet) and :overwritten (a golden existed and its bytes changed). A byte-golden clobber overwrites the ONLY reviewable record of what a real terminal would receive for that fixture x mode pair, so it must be loud at bless time, not just visible later in a PR diff of an opaque binary file -- :overwritten results carry a GoldenDiff report of old-vs-new bytes in :diff and the old size in :old_bytes, and the mix task prints both immediately.

Next to every <fixture>.<mode>.golden, bless also (re)writes <fixture>.<mode>.golden.txt: a line-oriented, inspect/1-escaped textual rendering of the same bytes (see escape_lines/1) that IS reviewable in a PR diff, even though the .golden file itself is marked binary in .gitattributes (deliberately -- its \r\n bytes are sealed-line protocol bytes, not line endings, and eol-normalizing them would corrupt the golden). A missing or stale sidecar is :drift in --check mode, exactly like a missing or stale golden.

Summary

Functions

Pure, line-oriented, inspect/1-escaped textual rendering of a raw golden byte stream -- the PR-reviewable form bless writes to <golden path>.txt next to every byte-opaque .golden file.

The fixture base names this matrix covers (see the moduledoc).

The on-disk path for a fixture x mode pair's golden file, e.g. test/fixtures/harness/goldens/simple-chat.inline_log.golden. Always resolved against goldens_dir/0 -- run/1's internal :dir option (mirroring Fixture.Bless.run/1's own :dir convention) does not affect this public helper, which existing tests use to read the checked-in goldens directly.

Directory holding the checked-in golden byte files.

Step budget for drive_to_completion/3, given the number of fixture events a render will replay.

The degradation-ladder tiers this matrix covers (see the moduledoc).

Renders session_or_name through the assembled Raxol.Harness.Surface in mode, at this matrix's fixed geometry, and returns the raw emitted byte stream. session_or_name is either an already-loaded %Raxol.Harness.Fixture.Session{} or a fixture base name (loaded from test/fixtures/harness/sessions/<name>.jsonl).

Runs the full fixtures x modes matrix.

Types

result()

@type result() :: %{
  name: String.t(),
  path: Path.t(),
  bytes: non_neg_integer(),
  status: status(),
  diff: String.t() | nil,
  old_bytes: non_neg_integer() | nil
}

status()

@type status() :: :created | :overwritten | :current | :drift

Functions

escape_lines(raw)

@spec escape_lines(binary()) :: String.t()

Pure, line-oriented, inspect/1-escaped textual rendering of a raw golden byte stream -- the PR-reviewable form bless writes to <golden path>.txt next to every byte-opaque .golden file.

raw is split into chunks at each \n byte (the newline stays attached to the chunk it ends -- a chunk NEVER starts with a leftover fragment of the previous line), each chunk is rendered via inspect(chunk, limit: :infinity, printable_limit: :infinity) (a literal Elixir term -- a quoted string or a <<...>> binary literal -- that reproduces the chunk exactly, unabridged), one per line, joined by a real \n, with one trailing \n.

Concatenating the unescaped form of every emitted line (each parsed back, e.g. via Code.eval_string/1) reproduces raw byte-for-byte -- see the round-trip property test in test/harness/golden_snapshot_test.exs. An empty raw produces zero escaped lines: just the single trailing \n.

fixtures()

@spec fixtures() :: [String.t()]

The fixture base names this matrix covers (see the moduledoc).

golden_path(fixture_name, mode)

@spec golden_path(String.t(), Raxol.Harness.Surface.mode()) :: Path.t()

The on-disk path for a fixture x mode pair's golden file, e.g. test/fixtures/harness/goldens/simple-chat.inline_log.golden. Always resolved against goldens_dir/0 -- run/1's internal :dir option (mirroring Fixture.Bless.run/1's own :dir convention) does not affect this public helper, which existing tests use to read the checked-in goldens directly.

goldens_dir()

@spec goldens_dir() :: Path.t()

Directory holding the checked-in golden byte files.

max_steps(event_count)

@spec max_steps(non_neg_integer()) :: pos_integer()

Step budget for drive_to_completion/3, given the number of fixture events a render will replay.

Derivation: Surface.advance/2 reveals at most one additional event per call (revealed = min(revealed + 1, length(events))), so at most event_count advances are ever needed to reveal every event. A render only reaches :done once revealed events AND painted blocks both catch up (painted_count trails revealed by at most one flushed block per advance), so the budget doubles event_count to also cover those painting-catchup advances, plus a flat 32-step buffer for advances that do not correspond to a revealed event at all (the startup footer paint, the mode-notice paint, and any other one-shot bookkeeping step).

modes()

@spec modes() :: [Raxol.Harness.Surface.mode()]

The degradation-ladder tiers this matrix covers (see the moduledoc).

render(name, mode)

Renders session_or_name through the assembled Raxol.Harness.Surface in mode, at this matrix's fixed geometry, and returns the raw emitted byte stream. session_or_name is either an already-loaded %Raxol.Harness.Fixture.Session{} or a fixture base name (loaded from test/fixtures/harness/sessions/<name>.jsonl).

See the moduledoc's "Determinism audit" for why this is safe to compare byte-for-byte across runs/machines: no wall clock, no live environment, no capability probe, fixed geometry. The replay loop is bounded (see drive_to_completion/3 / max_steps/1) rather than an unbounded recursion, so a non-converging Surface.advance/2 raises loudly instead of hanging.

run(opts \\ [])

@spec run(keyword()) :: {:ok, [result()]} | {:error, {:drift, [String.t()]}}

Runs the full fixtures x modes matrix.

Options:

  • :check (default false) -- when true, writes nothing: compares each fresh render against its on-disk golden AND its on-disk escaped textual sidecar (<path>.txt, see escape_lines/1), reporting :drift for either one missing or stale.
  • :dir (default goldens_dir/0) -- directory holding the golden (+ sidecar) files, mirroring Fixture.Bless.run/1's own :dir convention. Exists mainly so error-handling behavior (Fix 4: a directory occupying a golden's path) is unit-testable against a scratch directory rather than the checked-in one.

When :check is false (the bless path): writes a golden that doesn't exist yet (status :created), overwrites one whose bytes changed (status :overwritten -- see the moduledoc's "Bless status conventions"), or reports :current when the freshly rendered bytes already match what's on disk (a stale/missing sidecar is silently repaired in that case; the pair's status stays :current since the GOLDEN itself did not change). Sidecar (re)writes always accompany a golden create/overwrite.

Returns {:ok, [result()]} when nothing drifted (or, in bless mode, always -- writing resolves drift by construction), or {:error, {:drift, names}} in check mode when one or more fixture x mode pairs drifted, where names are "<fixture>.<mode>" strings.