11 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.
Progress (2026-06-29)
- Phase 0 DONE (commit
297bdc60) —web/tests/test-relate-picker.sx: relate→delete row drives the real engine (process-elements → submit → mock fetch → delete swap) against the OCaml runner's mock DOM, green. Mock-DOM completeness added torun_tests.ml:NodeList.item(i)(sodom-query-alliterates) + aDOMParsermock (so the empty-bodysx-swap=deleteHTML-response path works as in a browser). - Phase 1 DONE (commit
fe2da2d3) — same file, load / filter / paging / error-retry, 5/5 green, zero harness noise. Modelled two browser natives the OCaml runner lacks:observe-intersection(a recording stub the test fires to simulate the sentinel scrolling into view) and synchronous-timer retry (stripped in the error test — backoff math is atest-engine.sxconcern). Mock-DOM:firstChild/lastChild(sochildren-to-fragmentdrains a parsed fragment into innerHTML/outerHTML swaps; also repaired one pre-existing web test). No web-suite regressions.- Key seam discovered: a top-level
(define …)override is seen by engine library functions ONLY when the symbol lives in a different library than the caller (cross-library late-binds through global; same-library resolves locally).fetch-request(boot-helpers) overrides fine from a test;handle-retry(orchestration, same lib asdo-fetch) does NOT — hence the strip-attr approach. - harness-web.sx is NOT loaded by the OCaml runner (only the JS runner), and its
assertions assume a different mock-element shape (
attrs/text) than the OCaml mock DOM (attributes/textContent). Assert through the engine's owndom-*accessors instead.
- Key seam discovered: a top-level
- Phase 2 DONE (commit
98ff7a35) — Playwright trimmed 11 → 5 tests, both ephemeral suites green (run-spa-check 3/3, run-picker-check 2/2). Kept: WASM boot + content-addressed module loading (new/sx/h/assertion) + boosted nav swap + back/re-boost (spa-check); bind-boost-form remove button + picker re-bind after a boosted SPA nav (relate-picker). Deleted the populate/filter/paging/relate-delete/ error-retry browser tests (now SX). - Phase 3 (stretch) — the console platform — NOT STARTED.
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.