Greenfield SX-native pivot (NOT a strangler): the host speaks SX/SXTP end to end;
JSON only at the future ActivityPub federation edge.
- OUTPUT: host/json-status -> host/sx-status — every host/ok/host/error response is
text/sx via the serialize primitive (NOT application/json). Flips feed, relations,
blog reads. Tests assert the SX envelope ({:ok true :data ...}).
- DELETE the blog JSON CRUD /posts (POST/PUT/DELETE) + bearer-based host/blog--protect:
a pure old-contract REST mirror. Create/edit go through the HTML editor forms;
programmatic writes speak SXTP. FOLLOW-UP: no browser delete route yet (was JSON-only,
no UI) — add POST /:slug/delete + cascade edge cleanup when the metamodel UI needs it.
- INPUT: host/sx-body (sxtp.sx) parses a text/sx request body to a string-keyed dict
(parse-safe + sxtp/-normalize). feed POST + relations attach/detach read it.
- UNIFIED field reader host/fields / host/field: text/sx body OR urlencoded form by
content-type. The blog form handlers (new/edit/relate/unrelate) + login read through
it — additive, urlencoded still works (no-engine / bootstrap fallback).
Conformance 290/290 (11 suites). Retires the strangler framing in the plan; adds the
'SX all the way out' wire table. The engine half (browser posts text/sx) follows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
4.1 KiB
Plaintext
133 lines
4.1 KiB
Plaintext
;; lib/host/tests/feed.sx — the migrated feed endpoints, GET /feed (read) and
|
|
;; POST /feed (guarded write). Includes a golden test: the host read 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 marker precedes the oldest.
|
|
(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")))
|
|
(serialize {:ok true :data (feed/items (feed/recent (feed/all)))}))
|
|
(host-fd-test
|
|
"golden actor-filtered"
|
|
(dream-resp-body (host-fd-app (host-fd-req "/feed?actor=alice")))
|
|
(serialize {:ok true :data (feed/items (feed/by-actor (feed/recent (feed/all)) "alice"))}))
|
|
|
|
;; ── write: POST /feed (auth + ACL + action) ────────────────────────
|
|
(acl/load! (list (acl-grant "alice" "post" "feed")))
|
|
(define host-fd-resolve (fn (tok) (if (= tok "good") "alice" nil)))
|
|
(define
|
|
host-fd-wapp
|
|
(host/make-app
|
|
(list host/feed-routes (host/feed-write-routes host-fd-resolve))))
|
|
(define
|
|
host-fd-post
|
|
(fn (auth body)
|
|
(dream-request "POST" "/feed" (if auth {:authorization auth} {}) body)))
|
|
|
|
(feed/reset!)
|
|
(host-fd-test
|
|
"post no auth -> 401"
|
|
(dream-status (host-fd-wapp (host-fd-post nil "{}")))
|
|
401)
|
|
(host-fd-test
|
|
"post unchanged feed after 401"
|
|
(feed/size)
|
|
0)
|
|
(host-fd-test
|
|
"post authed+permitted -> 201"
|
|
(dream-status
|
|
(host-fd-wapp
|
|
(host-fd-post
|
|
"Bearer good"
|
|
"{:actor \"alice\" :verb \"post\" :object \"p9\" :at 9}")))
|
|
201)
|
|
(host-fd-test "post grew feed" (feed/size) 1)
|
|
(host-fd-test
|
|
"created activity visible in timeline"
|
|
(contains?
|
|
(dream-resp-body (host-fd-wapp (host-fd-req "/feed")))
|
|
"p9")
|
|
true)
|
|
(host-fd-test
|
|
"post non-object body -> 400"
|
|
(dream-status (host-fd-wapp (host-fd-post "Bearer good" "(1 2)")))
|
|
400)
|
|
|
|
(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}))
|