Files
rose-ash/plans/sx-native-engine-tests.md
giles b0c0fdd4b1
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
plan: Phase 3 target is the CONSOLE — engine renders the same picker to a terminal
The non-browser platform is a console/TUI renderer: the engine's platform ops map
to a text-node tree (harness-web's mock DOM is ~90% there), render-to-console
prints it, a raw-stdin input loop drives simulate-click/input. The same
~relate-picker runs unchanged in a terminal — browser is one platform binding,
console another, test harness a third.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:11:51 +00:00

8.8 KiB

Plan: SX-native engine tests (browser-independent)

Goal

Move the host's interactive test coverage from Playwright (.spec.js, drives a real Chromium) into SX harness tests that drive the hypermedia engine against a mock platform — no browser. Reserve Playwright for the one irreducible real-browser fact: "the WASM kernel actually compiles, boots, and loads modules content-addressed."

Why (the principle): the SX engine (web/engine.sx + web/orchestration.sx) has no hard browser dependency — it talks to a platform (fetch, DOM ops, timers) that is injected. The harness supplies a mock platform, so engine behaviour (fetch → swap → DOM mutation) is asserted with zero browser. The same engine could therefore drive something else (a server-side DOM, a native UI) — the SX tests prove that independence by running without one. This is consistent with [[project_zero_dependencies]] and [[feedback_runtime_control]] (build IN the runtime).

Current state (2026-06-29)

  • Already SX: the 272 host conformance tests (lib/host/tests/*.sx, spec/harness.sx mock-IO). The picker's server contract is SX too (lib/host/tests/blog.sx: picker form declaratively wired, load-more sentinel, no-sentinel-on-short-page).
  • Still Playwright (.spec.js): lib/host/playwright/relate-picker.spec.js (7 tests) and spa-check.spec.js (4) — real-browser checks of populate / filter / paging / relate-delete / remove-button / boosted-nav / error-retry / WASM boot.

Infrastructure that already exists (the enabler — verified)

  • spec/harness.sxmake-harness, default-platform with :fetch overridable ((fn (url &rest opts) {:status 200 :body "" :ok true})), plus DOM ops, :now, etc.
  • web/harness-web.sx(define-library (sx harness-web)) exports: mock-element, mock-set-attr!, mock-append-child!, mock-get-attr, mock-add-listener!, simulate-click / simulate-input / simulate-event, assert-text, assert-attr, assert-class, assert-no-class, assert-child-count, assert-event-fired, make-web-harness, render-audit helpers.
  • web/tests/ — existing SX engine tests: test-orchestration.sx (17 deftests), test-forms.sx (25), test-swap-integration.sx (43, mock-response → swap → assert), test-engine.sx, test-handlers.sx. test-swap-integration.sx is the reference pattern (it sets _mock-body/_mock-headers/_mock-content-type, drives a swap, asserts the result).
  • Runner: hosts/ocaml/bin/run_tests.ml scans spec/tests/, lib/tests/, web/tests/ and loads harness-web.sx + harness-reactive.sx. Run via the sx_test host="ocaml" MCP tool (or ./scripts/sx-build-all.sh). JS runner: hosts/javascript/run_tests.js also loads the web harnesses.

Phases

Phase 0 — Proof of concept (small): one behavior, SX

Port relate → delete row to an SX harness test (new web/tests/test-relate-picker.sx):

  1. Build a mock DOM: a .rp-results <ul> containing one candidate <li id="cand-related-x"> with the relate <form sx-post=/x/relate sx-target=#cand-related-x sx-swap=delete>.
  2. process-elements (or bind-triggers) the tree so the form's submit is bound.
  3. Mock :fetch to return {:status 200 :ok true :body ""}.
  4. simulate-click the button (or simulate-event "submit" on the form).
  5. Assert the <li> is gone (assert-child-count results = 0). This validates the mock-DOM → execute-request → swap-dom-nodes loop in SX end to end. If it reads cleanly, the rest is mechanical.

Phase 1 — Port the picker's interactive behaviors (medium)

Same file, more deftests, each = mock fetch + simulate + assert:

  • filter narrows: :fetch returns N candidate rows for q=...; simulate-input the filter; assert child-count == N.
  • sentinel paging: :fetch returns rows + a <li class=rp-more sx-trigger=revealed>; fire the revealed/intersect path; assert more rows appended, sentinel replaced.
  • load populate: load trigger → fetch → assert results filled.
  • error/retry visible state: :fetch rejects → assert .sx-error class added (assert-class), then succeeds → assert cleared.

Phase 2 — Trim Playwright to a boot smoke (small)

Keep ONLY what needs a real browser in relate-picker.spec.js / spa-check.spec.js:

  • WASM kernel compiles + boots (data-sx-ready).
  • modules load content-addressed (/sx/h/ fetches, 0 path .sxbc).
  • one boosted nav swaps #content. Delete the per-behavior browser tests now covered by SX. Net: ~2 browser tests + an SX suite.

Phase 3 — The engine drives the CONSOLE (the non-browser target)

The concrete "something else" is a terminal / console platform. This is the natural sibling of the test harness: a harness test asserts the engine's output tree; the console platform renders that same tree to text. Same platform abstraction — one observes it, one draws it.

What it means concretely:

  • Platform ops → a console-backed element tree. The engine only ever calls platform primitives: dom-create-element, dom-append, dom-set-attr, dom-query (by id, for sx-target), dom-remove-child, dom-parent, morph-children, dom-listen, fetch, set-timeout. Implement these against an in-memory tree of text nodes instead of the browser DOM. The mock DOM in web/harness-web.sx is ~90% of this already.
  • Render = print the tree as text (ANSI/box-drawing) — a render-to-console mode alongside render-to-html / render-to-dom (see spec/render.sx's mode table). The results <ul> becomes a list; .sx-error becomes a red line; the filter input is a text field.
  • Events = a TUI input loop. Keypresses / selection map to simulate-input / simulate-click on the focused node — exactly the harness's simulate-*, but driven by a real keyboard instead of a test.
  • fetch stays HTTP (the host already serves text/sx fragments + relate-options), or talks to a local store.

Payoff: the same ~relate-pickersx-get, debounced filter, revealed paging, sx-swap=delete, sx-error retry — runs unchanged in a terminal. That is the proof that the SX hypermedia engine is a general runtime, not a browser library: the browser is just one platform binding, the console is another, the test harness is a third. Ambitious, buildable, and the most convincing demonstration of the whole architecture ([[feedback_runtime_control]], [[project_zero_dependencies]]).

Sketch of work: (1) a console-platform.sx implementing the platform ops over a text tree (fork harness-web.sx's mock element), (2) a render-to-console mode in render.sx, (3) a tiny input loop (raw-mode stdin → focus model → simulate-*), (4) run the host's picker against it. Phase 1's SX tests become the regression suite for the console renderer for free (they already drive the tree, just don't print it).

Gaps & risks to resolve during Phase 0

  • Mock-DOM completeness: swap-dom-nodes uses morph-children, dom-replace-child, dom-insert-after/before/prepend/append, dom-remove-child, dom-parent, dom-first-child, dom-clone, dom-is-fragment?. Confirm harness-web's mock DOM implements (or can be extended for) these. test-swap-integration.sx already swaps, so most exist; check delete/outerHTML/fragment paths specifically.
  • fetch callback shape: the engine's fetch-request calls back (resp-ok status get-header text); the platform :fetch returns {:status :body :ok}. Confirm/adapt the bridge (see how test-swap-integration.sx feeds _mock-body etc.).
  • trigger binding without a browser: simulate-click fires bound listeners — the form must be processed first (process-elements on the mock root, or bind directly).
  • component expansion: ~relate-picker need not be expanded for these tests — assert on the rendered candidate rows / form markup directly (build the mock DOM from the expanded HTML the server produces, which is already SX-testable server-side).

Tracked loose ends (separate from this plan)

  • unrelate "clever" in-place delete (just-the-row, no #content re-render): now that bind-boost-form is fixed the remove button works via a boosted POST→swap; the minimal-mutation version (sx-post + sx-swap=delete on the current-row) is a further refinement — earlier attempt didn't fire, revisit with the binding now understood.
  • hs-repeat-times bytecode test (architecture worktree): harness host-new stub bug masks a pre-existing beingTold resume-env bug. See the diagnosis in this session.

Done-when

  • web/tests/test-relate-picker.sx covers populate / filter / paging / relate-delete / error-retry in SX, green under sx_test host="ocaml".
  • Playwright trimmed to the boot smoke; suite still green.
  • (Stretch) the picker runs through a non-browser platform.