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>
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.sxmock-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) andspa-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.sx—make-harness,default-platformwith:fetchoverridable ((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.sxis the reference pattern (it sets_mock-body/_mock-headers/_mock-content-type, drives a swap, asserts the result).- Runner:
hosts/ocaml/bin/run_tests.mlscansspec/tests/,lib/tests/,web/tests/and loadsharness-web.sx+harness-reactive.sx. Run via thesx_test host="ocaml"MCP tool (or./scripts/sx-build-all.sh). JS runner:hosts/javascript/run_tests.jsalso 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):
- 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>. process-elements(orbind-triggers) the tree so the form's submit is bound.- Mock
:fetchto return{:status 200 :ok true :body ""}. simulate-clickthe button (orsimulate-event"submit" on the form).- Assert the
<li>is gone (assert-child-countresults = 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:
:fetchreturns N candidate rows forq=...;simulate-inputthe filter; assert child-count == N. - sentinel paging:
:fetchreturns rows + a<li class=rp-more sx-trigger=revealed>; fire the revealed/intersect path; assert more rows appended, sentinel replaced. - load populate:
loadtrigger → fetch → assert results filled. - error/retry visible state:
:fetchrejects → assert.sx-errorclass 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, forsx-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 inweb/harness-web.sxis ~90% of this already. - Render = print the tree as text (ANSI/box-drawing) — a
render-to-consolemode alongsiderender-to-html/render-to-dom(seespec/render.sx's mode table). The results<ul>becomes a list;.sx-errorbecomes a red line; the filter input is a text field. - Events = a TUI input loop. Keypresses / selection map to
simulate-input/simulate-clickon the focused node — exactly the harness'ssimulate-*, but driven by a real keyboard instead of a test. fetchstays HTTP (the host already servestext/sxfragments +relate-options), or talks to a local store.
Payoff: the same ~relate-picker — sx-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-nodesusesmorph-children,dom-replace-child,dom-insert-after/before/prepend/append,dom-remove-child,dom-parent,dom-first-child,dom-clone,dom-is-fragment?. Confirmharness-web's mock DOM implements (or can be extended for) these.test-swap-integration.sxalready swaps, so most exist; checkdelete/outerHTML/fragment paths specifically. - fetch callback shape: the engine's
fetch-requestcalls back(resp-ok status get-header text); the platform:fetchreturns{:status :body :ok}. Confirm/adapt the bridge (see howtest-swap-integration.sxfeeds_mock-bodyetc.). - trigger binding without a browser:
simulate-clickfires bound listeners — the form must be processed first (process-elementson the mock root, or bind directly). - component expansion:
~relate-pickerneed 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
#contentre-render): now thatbind-boost-formis fixed the remove button works via a boosted POST→swap; the minimal-mutation version (sx-post +sx-swap=deleteon the current-row) is a further refinement — earlier attempt didn't fire, revisit with the binding now understood. hs-repeat-timesbytecode test (architecture worktree): harnesshost-newstub bug masks a pre-existingbeingToldresume-env bug. See the diagnosis in this session.
Done-when
web/tests/test-relate-picker.sxcovers populate / filter / paging / relate-delete / error-retry in SX, green undersx_test host="ocaml".- Playwright trimmed to the boot smoke; suite still green.
- (Stretch) the picker runs through a non-browser platform.