;; lib/host/tests/sxtp.sx — SXTP message algebra, wire serialise/parse round-trip, ;; and the Dream HTTP <-> SXTP bridge. (define host-sx-pass 0) (define host-sx-fail 0) (define host-sx-fails (list)) (define host-sx-test (fn (name actual expected) (if (= actual expected) (set! host-sx-pass (+ host-sx-pass 1)) (begin (set! host-sx-fail (+ host-sx-fail 1)) (append! host-sx-fails {:name name :actual actual :expected expected}))))) ;; ── constructors + predicates ────────────────────────────────────── (define host-sx-req (sxtp/request "navigate" "/x" {:headers {:host "h"}})) (define host-sx-resp (sxtp/ok {:id "e1"})) (host-sx-test "request?" (sxtp/request? host-sx-req) true) (host-sx-test "request not response" (sxtp/response? host-sx-req) false) (host-sx-test "response?" (sxtp/response? host-sx-resp) true) (host-sx-test "condition?" (sxtp/condition? (sxtp/condition "x" {})) true) (host-sx-test "patch?" (sxtp/patch? (sxtp/patch "#x" {})) true) (host-sx-test "patch not event" (sxtp/event? (sxtp/patch "#x" {})) false) (host-sx-test "signals?" (sxtp/signals? (sxtp/signals {:n 3} {})) true) (host-sx-test "signals not patch" (sxtp/patch? (sxtp/signals {:n 3} {})) false) ;; ── accessors (verb/status are symbols) ──────────────────────────── (host-sx-test "verb" (symbol->string (sxtp/verb host-sx-req)) "navigate") (host-sx-test "path" (sxtp/path host-sx-req) "/x") (host-sx-test "req header" (get (sxtp/req-headers host-sx-req) :host) "h") (host-sx-test "status" (symbol->string (sxtp/status host-sx-resp)) "ok") (host-sx-test "body" (get (sxtp/body host-sx-resp) :id) "e1") ;; ── status helpers ───────────────────────────────────────────────── (host-sx-test "created status" (symbol->string (sxtp/status (sxtp/created {}))) "created") (host-sx-test "not-found status" (symbol->string (sxtp/status (sxtp/not-found "/p" "gone"))) "not-found") (host-sx-test "not-found body is condition" (sxtp/condition? (sxtp/body (sxtp/not-found "/p" "gone"))) true) (host-sx-test "forbidden message" (sxtp/cond-message (sxtp/body (sxtp/forbidden "no"))) "no") ;; ── serialise (deterministic top-level field order) ──────────────── (host-sx-test "serialize request" (sxtp/serialize host-sx-req) "(request :verb navigate :path \"/x\" :headers {:host \"h\"})") (host-sx-test "serialize ok" (sxtp/serialize (sxtp/ok {:id "e1"})) "(response :status ok :body {:id \"e1\"})") ;; nested condition rides the wire in its (condition ...) list form, no :msg leak. (host-sx-test "serialize nested condition as list" (contains? (sxtp/serialize (sxtp/not-found "/p" "gone")) "(condition :type resource-not-found") true) (host-sx-test "serialize no :msg leak" (contains? (sxtp/serialize host-sx-resp) ":msg") false) ;; ── patch + signals (Datastar-borrowed) ─────────────────────────── ;; Mode defaults to outer; accepts string OR symbol input. (host-sx-test "patch default mode is outer symbol" (symbol->string (sxtp/mode (sxtp/patch "#x" {}))) "outer") (host-sx-test "patch accepts symbol mode" (symbol->string (sxtp/mode (sxtp/patch "#x" {:mode (string->symbol "inner")}))) "inner") (host-sx-test "patch accepts string mode and normalises" (symbol->string (sxtp/mode (sxtp/patch "#x" {:mode "append"}))) "append") (host-sx-test "patch target accessor" (sxtp/target (sxtp/patch "#cart" {})) "#cart") (host-sx-test "patch serialises with target/mode/body in fixed order" (sxtp/serialize (sxtp/patch "#x" {:body "hi"})) "(patch :target \"#x\" :mode outer :body \"hi\")") (host-sx-test "patch remove mode serialises without :body" (sxtp/serialize (sxtp/patch "#x" {:mode "remove"})) "(patch :target \"#x\" :mode remove)") (host-sx-test "patch transition? predicate" (sxtp/transition? (sxtp/patch "#x" {:transition true})) true) (host-sx-test "signals accessor" (get (sxtp/values (sxtp/signals {:cart/count 3} {})) :cart/count) 3) (host-sx-test "signals only-if-missing default false" (sxtp/only-if-missing? (sxtp/signals {:n 1} {})) false) (host-sx-test "signals only-if-missing true round-trips" (sxtp/only-if-missing? (sxtp/signals {:n 1} {:only-if-missing true})) true) (host-sx-test "signals serialise" (sxtp/serialize (sxtp/signals {:cart/count 3} {})) "(signals :values {:cart/count 3})") ;; ── round-trip ──────────────────────────────────────────────────── (define host-sx-patch-rt (sxtp/parse (sxtp/serialize (sxtp/patch "#mini" {:mode "inner" :body "n=3"})))) (host-sx-test "patch rt msg" (sxtp/patch? host-sx-patch-rt) true) (host-sx-test "patch rt target" (sxtp/target host-sx-patch-rt) "#mini") (host-sx-test "patch rt mode" (symbol->string (sxtp/mode host-sx-patch-rt)) "inner") (define host-sx-signals-rt (sxtp/parse (sxtp/serialize (sxtp/signals {:a 1 :b "x"} {:only-if-missing true})))) (host-sx-test "signals rt msg" (sxtp/signals? host-sx-signals-rt) true) (host-sx-test "signals rt values" (get (sxtp/values host-sx-signals-rt) :a) 1) (host-sx-test "signals rt only-if-missing" (sxtp/only-if-missing? host-sx-signals-rt) true) ;; ── parse + round-trip ───────────────────────────────────────────── (define host-sx-parsed (sxtp/parse "(request :verb query :path \"/events\" :headers {:host \"h\"})")) (host-sx-test "parse msg type" (sxtp/request? host-sx-parsed) true) (host-sx-test "parse verb" (symbol->string (sxtp/verb host-sx-parsed)) "query") (host-sx-test "parse path" (sxtp/path host-sx-parsed) "/events") (host-sx-test "parse nested header normalised" (get (sxtp/req-headers host-sx-parsed) :host) "h") (define host-sx-rt (sxtp/parse (sxtp/serialize (sxtp/ok {:id "e1" :n 3})))) (host-sx-test "round-trip status" (symbol->string (sxtp/status host-sx-rt)) "ok") (host-sx-test "round-trip body id" (get (sxtp/body host-sx-rt) :id) "e1") (host-sx-test "round-trip body n" (get (sxtp/body host-sx-rt) :n) 3) ;; ── HTTP <-> SXTP mappings ───────────────────────────────────────── (host-sx-test "verb GET->fetch" (symbol->string (sxtp/verb-for-method "GET")) "fetch") (host-sx-test "verb POST->create" (symbol->string (sxtp/verb-for-method "POST")) "create") (host-sx-test "verb DELETE->delete" (symbol->string (sxtp/verb-for-method "DELETE")) "delete") (host-sx-test "verb unknown->fetch" (symbol->string (sxtp/verb-for-method "WIBBLE")) "fetch") (host-sx-test "http ok->200" (sxtp/http-status (string->symbol "ok")) 200) (host-sx-test "http not-found->404" (sxtp/http-status (string->symbol "not-found")) 404) ;; ── Dream bridge ─────────────────────────────────────────────────── (define host-sx-from (sxtp/from-dream (dream-request "POST" "/feed?a=1" {} "hi"))) (host-sx-test "from-dream verb" (symbol->string (sxtp/verb host-sx-from)) "create") (host-sx-test "from-dream path" (sxtp/path host-sx-from) "/feed") (host-sx-test "from-dream param" (sxtp/param host-sx-from "a") "1") (host-sx-test "from-dream body" (sxtp/body host-sx-from) "hi") (define host-sx-tod (sxtp/to-dream (sxtp/ok {:id "e1"}))) (host-sx-test "to-dream status" (dream-status host-sx-tod) 200) (host-sx-test "to-dream content-type text/sx" (dream-resp-header host-sx-tod "content-type") "text/sx") (host-sx-test "to-dream body is sx text" (dream-resp-body host-sx-tod) "{:id \"e1\"}") (host-sx-test "to-dream not-found->404" (dream-status (sxtp/to-dream (sxtp/not-found "/p" "gone"))) 404) (host-sx-test "to-dream forbidden->403" (dream-status (sxtp/to-dream (sxtp/forbidden "no"))) 403) ;; ── engine<->server write wire: serialize (engine) <-> host/sx-body (server) ── ;; A boosted form posts (serialize {field->value}) as text/sx; the server reads it ;; back with host/sx-body. This is the SX write wire, verified with NO DOM (client- ;; agnostic): what the engine's serialize emits, host/sx-body must parse back ;; losslessly — including sx_content full of the quotes/parens that would break a ;; naive encoder. (The server side is what conformance can prove; the DOM field-read ;; is the one irreducibly-browser bit, left to a Playwright smoke.) (define host-sx-wire-content "(article (h1 \"Title\") (p \"He said \\\"hi\\\" (x)\"))") (define host-sx-wire-req (dream-request "POST" "/x" {:content-type "text/sx"} (serialize {:title "Hi there" :sx_content host-sx-wire-content :status "published"}))) (host-sx-test "sx-body round-trips a serialized field dict" (get (host/sx-body host-sx-wire-req) "title") "Hi there") (host-sx-test "sx-body preserves quoted/parenthesised sx_content losslessly" (get (host/sx-body host-sx-wire-req) "sx_content") host-sx-wire-content) (host-sx-test "field reads a text/sx body by content-type" (host/field host-sx-wire-req "status") "published") (host-sx-test "field falls back to urlencoded form (the no-engine path)" (host/field (dream-request "POST" "/x" {:content-type "application/x-www-form-urlencoded"} "title=From+Form&status=draft") "title") "From Form") (define host-sx-tests-run! (fn () {:total (+ host-sx-pass host-sx-fail) :passed host-sx-pass :failed host-sx-fail :fails host-sx-fails}))