host: Phase 1 — router + handler + GET /feed endpoint on Dream, 28/28

First migrated endpoint onto the SX host. lib/host is a thin wiring layer:
a host handler is a Dream handler (request->response) that calls a subsystem
public API and serialises via a shared JSON envelope.

- handler.sx: host/ok, host/ok-status, host/error, host/json-status (Dream's
  dream-json is 200-only), host/query-int
- router.sx: host/make-app assembles per-domain route groups + /health probe
  into one dream-router (reuses dr/flatten-routes)
- feed.sx: GET /feed reads feed/all + stream combinators, recent-first, with
  ?actor= filter and ?limit= cap
- 3 test suites incl. a golden test (body == subsystem recent stream + envelope)
- conformance.sh mirrors lib/dream's runner

Builds on dream-on-sx (merged, gate green 480/480) rather than a throwaway
native request model; collapses most of plan Phase 4 into Phase 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 19:36:55 +00:00
parent fe958bda69
commit d5a1c8370c
8 changed files with 480 additions and 7 deletions

98
lib/host/tests/feed.sx Normal file
View File

@@ -0,0 +1,98 @@
;; lib/host/tests/feed.sx — the first migrated endpoint, GET /feed. Includes a
;; golden test: the host response body must equal the feed subsystem's own
;; recent-first stream wrapped in the standard envelope — the endpoint adds the
;; HTTP/JSON shell and nothing else.
(define host-fd-pass 0)
(define host-fd-fail 0)
(define host-fd-fails (list))
(define
host-fd-test
(fn
(name actual expected)
(if
(= actual expected)
(set! host-fd-pass (+ host-fd-pass 1))
(begin
(set! host-fd-fail (+ host-fd-fail 1))
(append! host-fd-fails {:name name :actual actual :expected expected})))))
(define
host-fd-req
(fn (target) (dream-request "GET" target {} "")))
(define
host-fd-app
(host/make-app (list host/feed-routes)))
;; ── empty feed ─────────────────────────────────────────────────────
(feed/reset!)
(host-fd-test
"empty feed 200"
(dream-status (host-fd-app (host-fd-req "/feed")))
200)
(host-fd-test
"empty feed data:[]"
(contains? (dream-resp-body (host-fd-app (host-fd-req "/feed"))) "\"data\":[]")
true)
;; ── seeded feed ────────────────────────────────────────────────────
(feed/reset!)
(feed/post {:actor "alice" :verb "post" :object "p1" :at 1})
(feed/post {:actor "bob" :verb "post" :object "p2" :at 2})
(feed/post {:actor "alice" :verb "like" :object "p2" :at 3})
;; recent-first: newest activity (at 3) leads, so its object p2 appears before p1.
(host-fd-test
"timeline recent-first"
(let ((body (dream-resp-body (host-fd-app (host-fd-req "/feed")))))
(< (index-of body "\"at\":3") (index-of body "\"at\":1")))
true)
;; actor filter: only alice's two activities.
(host-fd-test
"actor filter count"
(feed/count
(feed/by-actor (feed/recent (feed/all)) "alice"))
2)
(host-fd-test
"actor filter excludes bob"
(contains?
(dream-resp-body (host-fd-app (host-fd-req "/feed?actor=alice")))
"bob")
false)
;; limit: cap to a single activity (the most recent).
(host-fd-test
"limit caps results"
(contains?
(dream-resp-body (host-fd-app (host-fd-req "/feed?limit=1")))
"\"at\":1")
false)
;; ── golden: endpoint = subsystem recent stream + envelope ───────────
(host-fd-test
"golden full timeline"
(dream-resp-body (host-fd-app (host-fd-req "/feed")))
(str
"{\"ok\":true,\"data\":"
(dream-json-encode (feed/items (feed/recent (feed/all))))
"}"))
(host-fd-test
"golden actor-filtered"
(dream-resp-body (host-fd-app (host-fd-req "/feed?actor=alice")))
(str
"{\"ok\":true,\"data\":"
(dream-json-encode
(feed/items (feed/by-actor (feed/recent (feed/all)) "alice")))
"}"))
(define
host-fd-tests-run!
(fn
()
{:total (+ host-fd-pass host-fd-fail)
:passed host-fd-pass
:failed host-fd-fail
:fails host-fd-fails}))

86
lib/host/tests/handler.sx Normal file
View File

@@ -0,0 +1,86 @@
;; lib/host/tests/handler.sx — host JSON envelope + request-reading helpers.
(define host-hd-pass 0)
(define host-hd-fail 0)
(define host-hd-fails (list))
(define
host-hd-test
(fn
(name actual expected)
(if
(= actual expected)
(set! host-hd-pass (+ host-hd-pass 1))
(begin
(set! host-hd-fail (+ host-hd-fail 1))
(append! host-hd-fails {:name name :actual actual :expected expected})))))
;; ── host/ok ────────────────────────────────────────────────────────
(host-hd-test "ok status 200" (dream-status (host/ok "x")) 200)
(host-hd-test
"ok content-type json"
(dream-resp-header (host/ok "x") "content-type")
"application/json")
(host-hd-test
"ok envelope ok:true"
(contains? (dream-resp-body (host/ok "x")) "\"ok\":true")
true)
(host-hd-test
"ok envelope carries data"
(contains? (dream-resp-body (host/ok "hi")) "\"data\":\"hi\"")
true)
;; ── host/ok-status ─────────────────────────────────────────────────
(host-hd-test "ok-status custom" (dream-status (host/ok-status 201 "y")) 201)
(host-hd-test
"ok-status data"
(contains? (dream-resp-body (host/ok-status 201 "y")) "\"data\":\"y\"")
true)
;; ── host/error ─────────────────────────────────────────────────────
(host-hd-test "error status" (dream-status (host/error 404 "nope")) 404)
(host-hd-test
"error ok:false"
(contains? (dream-resp-body (host/error 404 "nope")) "\"ok\":false")
true)
(host-hd-test
"error message"
(contains? (dream-resp-body (host/error 404 "nope")) "\"error\":\"nope\"")
true)
(host-hd-test
"error content-type json"
(dream-resp-header (host/error 500 "boom") "content-type")
"application/json")
;; ── host/json-status ───────────────────────────────────────────────
(host-hd-test
"json-status arbitrary status"
(dream-status (host/json-status 418 {:a 1}))
418)
(host-hd-test
"json-status encodes body"
(contains? (dream-resp-body (host/json-status 200 {:a 1})) "\"a\":1")
true)
;; ── host/query-int ─────────────────────────────────────────────────
(define
host-hd-req
(fn (target) (dream-request "GET" target {} "")))
(host-hd-test
"query-int present"
(host/query-int (host-hd-req "/x?limit=5") "limit" 10)
5)
(host-hd-test
"query-int absent -> fallback"
(host/query-int (host-hd-req "/x") "limit" 10)
10)
(define
host-hd-tests-run!
(fn
()
{:total (+ host-hd-pass host-hd-fail)
:passed host-hd-pass
:failed host-hd-fail
:fails host-hd-fails}))

75
lib/host/tests/router.sx Normal file
View File

@@ -0,0 +1,75 @@
;; lib/host/tests/router.sx — host app assembly: health endpoint, group mounting,
;; 404 fallback.
(define host-rt-pass 0)
(define host-rt-fail 0)
(define host-rt-fails (list))
(define
host-rt-test
(fn
(name actual expected)
(if
(= actual expected)
(set! host-rt-pass (+ host-rt-pass 1))
(begin
(set! host-rt-fail (+ host-rt-fail 1))
(append! host-rt-fails {:name name :actual actual :expected expected})))))
(define
host-rt-req
(fn (method target) (dream-request method target {} "")))
;; An app built from one domain group of two routes.
(define
host-rt-app
(host/make-app
(list
(list
(dream-get "/ping" (fn (req) (host/ok "pong")))
(dream-get "/widgets/:id" (fn (req) (host/ok (dream-param req "id"))))))))
;; ── health ─────────────────────────────────────────────────────────
(host-rt-test
"health status 200"
(dream-status (host-rt-app (host-rt-req "GET" "/health")))
200)
(host-rt-test
"health body healthy"
(contains?
(dream-resp-body (host-rt-app (host-rt-req "GET" "/health")))
"healthy")
true)
;; ── group routes mounted ───────────────────────────────────────────
(host-rt-test
"group route ping"
(contains?
(dream-resp-body (host-rt-app (host-rt-req "GET" "/ping")))
"pong")
true)
(host-rt-test
"group path param"
(contains?
(dream-resp-body (host-rt-app (host-rt-req "GET" "/widgets/42")))
"\"data\":\"42\"")
true)
;; ── fallback ───────────────────────────────────────────────────────
(host-rt-test
"unknown path 404"
(dream-status (host-rt-app (host-rt-req "GET" "/nope")))
404)
(host-rt-test
"wrong method 405"
(dream-status (host-rt-app (host-rt-req "POST" "/ping")))
405)
(define
host-rt-tests-run!
(fn
()
{:total (+ host-rt-pass host-rt-fail)
:passed host-rt-pass
:failed host-rt-fail
:fails host-rt-fails}))