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

View Source

Multi-surface parity matrix: renders each fixture session through all four surface projections and pins each one, so "one TEA module renders to terminal, browser, SSH, and MCP" is a checked claim rather than a README sentence.

Raxol.Harness.Surface.Golden pins the raw bytes of ONE surface (the inline/degradation terminal ladder) across three render modes. This module pins ONE render across four SURFACES, and additionally asserts they agree with each other. A regression that teaches the LiveView encoder to drop a wide character, or the ANSI writer to emit SGR before the cursor move, is invisible to a single-surface golden and lands here as both a drift and a parity break.

The shared pivot

Every projection derives from ONE render of ONE fold, so a divergence is always the projection's fault and never the input's:

session (.jsonl)
  -> Raxol.Harness.Projection.project/2   (pure fold, T7)
  -> [Block.t()] -> Block.render/2        (pure view tree)
  -> LayoutEngine.apply_layout/2          (positioned elements)
  -> UIRenderer.render_to_cells/2         (the cell grid)

and from the cell grid:

surfaceprojectionderived from
:cellscanonical row-major cell dumpthe grid
:liveview_domTerminalBridge.buffer_to_html/2, normalizedthe grid
:ssh_ansiCore.Renderer.render_diff/2 + apply_diff/1, escapedthe grid
:structured_jsonMCP.StructuredScreenshot.from_view_tree/2the view tree

:structured_json is deliberately taken from the view tree rather than the grid: that IS the MCP surface's input, and pinning it from the grid would test a pipeline nothing runs.

What parity means, per surface

:cells, :liveview_dom, and :ssh_ansi are three encodings of the same grid, so their visible text must be character-for-character identical -- parity/1 asserts exactly that, and it is the check with teeth: each encoder walks the grid independently.

:structured_json comes from the pre-layout tree, so its text is NOT grid-identical (layout wraps, truncates, and pads). Its parity property is containment, checked at the word level: a word the MCP surface reports must be visible on screen. Claiming more would be false precision.

Determinism

Same closure as Raxol.Harness.Surface.Golden's audit, plus:

  • fixed geometry (60x24), so layout never reads a live terminal;
  • an explicitly constructed theme, so the hash never depends on the global theme registry (which other tests in the suite mutate) -- the same reason Raxol.RATE passes one;
  • render_diff/2 is taken against a BLANK buffer, making the ANSI stream a full repaint rather than a function of whatever was on screen before;
  • cells are sorted row-major before serialization, so emission order cannot leak into the hash.

Summary

Functions

On-disk path for a fixture x surface artifact.

Every projectable fixture in test/fixtures/harness/sessions, sorted.

Fixed render geometry, shared by every surface.

Checks cross-surface agreement for fixture.

Directory holding the checked-in per-surface artifacts.

Project a rendered fixture onto one surface. Returns the artifact text.

Path of the committed hash refs file.

Renders fixture once and returns %{tree: view_tree, cells: cells} -- the single pivot every projection is taken from.

Blesses (or with check: true, verifies) every fixture x surface artifact and the hash refs file.

The four surface projections this matrix covers.

The visible text of a fixture on each grid-derived surface, as %{surface => [line]}. The three lists must be equal -- see parity/1.

Types

result()

@type result() :: %{
  name: String.t(),
  fixture: String.t(),
  surface: surface(),
  status: :current | :created | :overwritten,
  path: Path.t(),
  bytes: non_neg_integer()
}

surface()

@type surface() :: :cells | :liveview_dom | :ssh_ansi | :structured_json

Functions

artifact_path(fixture, surface)

@spec artifact_path(String.t(), surface()) :: Path.t()

On-disk path for a fixture x surface artifact.

fixtures()

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

Every projectable fixture in test/fixtures/harness/sessions, sorted.

Membership is decided by Raxol.Harness.Fixture.Session.golden?/1 -- the same predicate Raxol.Harness.Fixture.Bless uses, so the two corpora cannot drift apart. An kind: "adversarial" fixture exists to be REJECTED by the loader, so there is no render to pin.

Note this is a header field, not a filename convention: a .notes.md sidecar is documentation and says nothing about whether a fixture renders (projection-panels is hand-authored, documented, and golden).

geometry()

@spec geometry() :: %{width: pos_integer(), height: pos_integer()}

Fixed render geometry, shared by every surface.

parity(fixture)

@spec parity(String.t()) :: :ok | {:error, map()}

Checks cross-surface agreement for fixture.

Returns :ok, or {:error, diagnostics} naming the first disagreeing line per surface pair plus any MCP-reported word that is not on screen.

parity_dir()

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

Directory holding the checked-in per-surface artifacts.

project(fixture, surface)

@spec project(String.t() | map(), surface()) :: String.t()

Project a rendered fixture onto one surface. Returns the artifact text.

refs_path()

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

Path of the committed hash refs file.

render(fixture)

@spec render(String.t()) :: %{tree: map(), cells: list()}

Renders fixture once and returns %{tree: view_tree, cells: cells} -- the single pivot every projection is taken from.

run(opts \\ [])

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

Blesses (or with check: true, verifies) every fixture x surface artifact and the hash refs file.

Returns {:ok, results} or {:error, {:drift, names}}.

surfaces()

@spec surfaces() :: [surface()]

The four surface projections this matrix covers.

visible_text(fixture)

@spec visible_text(String.t() | map()) :: %{required(surface()) => [String.t()]}

The visible text of a fixture on each grid-derived surface, as %{surface => [line]}. The three lists must be equal -- see parity/1.