sx-gitea Phase 8: fed — ForgeFed federation (TDD, 615/615 all suites)
lib/gitea/fed.sx: forges federate as peers. Each forge carries an instance id; users and repos project as AP actor documents (Person/ Group/Repository with inbox/outbox + clone endpoint); the outbox is the activity log in an AP-shaped envelope. Trust follows the events-federation pattern — a kv set of peer ids RE-CHECKED on every operation (inbox, mirror sync, delivery), so revoking a peer takes effect immediately; peer transports (dream app fns) live only in the runtime cache. Inbox (POST /api/ap/inbox, trust-gated): every accepted activity lands in a federated log with :origin provenance; open-issue/comment/open-pr MATERIALIZE — the foreign author becomes an auto-created proxy user '<name>@<peer>' and the issue/comment/PR is created locally under that identity. fed-deliver! pushes public-repo activities (cursor-based, never private) to every trusted peer's inbox. Cross-instance repo follow = mirror!/mirror-sync! over the Phase 3 wire client. fed-timeline merges local + foreign activities with provenance tags. Suite: two in-memory forges federating end to end — actor docs, trust lifecycle, materialization, proxy-user reuse, wire inbox 400/403/200, mirrors (clone/sync/trust-revocation), cursor delivery, timelines. Adds lib/gitea/README.md (composition map, architectural rules, known limits). Final scoreboard: 615/615 across repo/access/wire/issues/pr/ activity/search/fed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
371
lib/gitea/tests/fed.sx
Normal file
371
lib/gitea/tests/fed.sx
Normal file
@@ -0,0 +1,371 @@
|
||||
; lib/gitea/tests/fed.sx — Phase 8: ForgeFed. Two in-memory forges:
|
||||
; AP actor docs + outbox, trust-gated inbox with provenance log and
|
||||
; materialized federated issues/comments/PRs (proxy users), cursor-based
|
||||
; outbound delivery, cross-instance repo mirrors, federated timeline.
|
||||
|
||||
(st-bootstrap-classes!)
|
||||
(content/bootstrap!)
|
||||
(content-bootstrap-markdown!)
|
||||
(content-bootstrap-table!)
|
||||
|
||||
(define gitea-fed-pass 0)
|
||||
(define gitea-fed-fail 0)
|
||||
(define gitea-fed-fails (list))
|
||||
|
||||
(define
|
||||
gitea-fed-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! gitea-fed-pass (+ gitea-fed-pass 1))
|
||||
(begin
|
||||
(set! gitea-fed-fail (+ gitea-fed-fail 1))
|
||||
(set! gitea-fed-fails (append gitea-fed-fails (list {:name name :expected (inspect expected) :actual (inspect actual)})))))))
|
||||
|
||||
; ── forge A ──────────────────────────────────────────────────────────
|
||||
|
||||
(define gf-dbA (persist/mem-backend))
|
||||
(define gf-A (gitea/forge gf-dbA))
|
||||
(gitea/instance! gf-A "forge-a")
|
||||
(gitea/user-create! gf-A "alice")
|
||||
(gitea/org-create! gf-A "acme")
|
||||
(gitea/repo-create! gf-A "alice" "lib" {:created-at 1})
|
||||
(gitea/repo-create! gf-A "alice" "hid" {:created-at 2 :visibility "private"})
|
||||
|
||||
(define gf-gA (gitea/repo-git gf-A "alice" "lib"))
|
||||
(git/add! gf-gA "README.md" "the lib\n")
|
||||
(git/commit! gf-gA {:message "c1" :time 3 :author "alice"})
|
||||
|
||||
(gitea/issue-create!
|
||||
gf-A
|
||||
"alice"
|
||||
"lib"
|
||||
"alice"
|
||||
"Public issue"
|
||||
"hello"
|
||||
{:created-at 4})
|
||||
(gitea/issue-create!
|
||||
gf-A
|
||||
"alice"
|
||||
"hid"
|
||||
"alice"
|
||||
"Hidden issue"
|
||||
"shh"
|
||||
{:created-at 5})
|
||||
|
||||
; ── forge B ──────────────────────────────────────────────────────────
|
||||
|
||||
(define gf-dbB (persist/mem-backend))
|
||||
(define gf-B (gitea/forge gf-dbB))
|
||||
(gitea/instance! gf-B "forge-b")
|
||||
(gitea/user-create! gf-B "bob")
|
||||
(gitea/repo-create! gf-B "bob" "hub" {:created-at 10})
|
||||
|
||||
(define gf-gB (gitea/repo-git gf-B "bob" "hub"))
|
||||
(git/add! gf-gB "main.txt" "hub main\n")
|
||||
(git/commit! gf-gB {:message "h1" :time 11 :author "bob"})
|
||||
(git/branch! gf-gB "feat")
|
||||
(git/checkout! gf-gB "feat")
|
||||
(git/add! gf-gB "feat.txt" "hub feat\n")
|
||||
(git/commit! gf-gB {:message "h2" :time 12 :author "bob"})
|
||||
(git/checkout! gf-gB "main")
|
||||
|
||||
(define gf-appA (gitea/app gf-A))
|
||||
(define gf-appB (gitea/app gf-B))
|
||||
(gitea/peer-register! gf-B "forge-a" gf-appA nil)
|
||||
(gitea/peer-register! gf-A "forge-b" gf-appB nil)
|
||||
|
||||
; ── identity + actor documents ───────────────────────────────────────
|
||||
|
||||
(gitea-fed-test "instance id" (gitea/instance-id gf-A) "forge-a")
|
||||
(gitea-fed-test
|
||||
"actor id"
|
||||
(gitea/actor-id gf-A "user:alice")
|
||||
"forge-a/user:alice")
|
||||
|
||||
(gitea-fed-test
|
||||
"ap user type"
|
||||
(get (gitea/ap-user gf-A "alice") :type)
|
||||
"Person")
|
||||
(gitea-fed-test
|
||||
"ap user id"
|
||||
(get (gitea/ap-user gf-A "alice") :id)
|
||||
"forge-a/user:alice")
|
||||
(gitea-fed-test
|
||||
"ap org type"
|
||||
(get (gitea/ap-user gf-A "acme") :type)
|
||||
"Group")
|
||||
(gitea-fed-test "ap user missing" (gitea/ap-user gf-A "zeb") nil)
|
||||
|
||||
(gitea-fed-test
|
||||
"ap repo type"
|
||||
(get (gitea/ap-repo gf-A "alice" "lib") :type)
|
||||
"Repository")
|
||||
(gitea-fed-test
|
||||
"ap repo attribution"
|
||||
(get (gitea/ap-repo gf-A "alice" "lib") :attributedTo)
|
||||
"forge-a/user:alice")
|
||||
(gitea-fed-test
|
||||
"ap repo clone endpoint"
|
||||
(get (gitea/ap-repo gf-A "alice" "lib") :clone)
|
||||
"/alice/lib/info/refs")
|
||||
|
||||
(gitea-fed-test
|
||||
"outbox is ap-shaped"
|
||||
(get (first (gitea/ap-outbox gf-A "alice" 10)) :actor)
|
||||
"forge-a/user:alice")
|
||||
(gitea-fed-test
|
||||
"outbox hides private repos"
|
||||
(len (gitea/ap-outbox gf-A "alice" 10))
|
||||
2)
|
||||
|
||||
; ── trust ────────────────────────────────────────────────────────────
|
||||
|
||||
(gitea-fed-test "untrusted by default" (gitea/trusted? gf-B "forge-a") false)
|
||||
(gitea-fed-test
|
||||
"inbox rejects untrusted"
|
||||
(get (gitea/fed-receive! gf-B "forge-a" {:verb "open-issue"}) :error)
|
||||
"untrusted-peer")
|
||||
(gitea-fed-test
|
||||
"rejected activity not logged"
|
||||
(len (gitea/fed-log gf-B))
|
||||
0)
|
||||
|
||||
(gitea/trust! gf-B "forge-a")
|
||||
(gitea-fed-test "trusted after trust!" (gitea/trusted? gf-B "forge-a") true)
|
||||
(gitea-fed-test "trusted peers" (gitea/trusted-peers gf-B) (list "forge-a"))
|
||||
|
||||
; ── inbound materialization ──────────────────────────────────────────
|
||||
|
||||
(define gf-r1 (gitea/fed-receive! gf-B "forge-a" {:actor "forge-a/user:alice" :detail {:title "Fed issue" :body "opened from forge-a"} :object "issue:bob/hub#0" :at 50 :tags (list "repo:bob/hub") :verb "open-issue"}))
|
||||
|
||||
(gitea-fed-test "fed issue accepted" (get gf-r1 :materialized) "issue")
|
||||
(gitea-fed-test "fed issue number" (get gf-r1 :number) 1)
|
||||
(gitea-fed-test
|
||||
"proxy user created"
|
||||
(gitea/owner-exists? gf-B "alice@forge-a")
|
||||
true)
|
||||
(gitea-fed-test
|
||||
"fed issue author"
|
||||
(get (gitea/issue-get gf-B "bob" "hub" 1) :author)
|
||||
"alice@forge-a")
|
||||
(gitea-fed-test
|
||||
"fed issue title"
|
||||
(get (gitea/issue-get gf-B "bob" "hub" 1) :title)
|
||||
"Fed issue")
|
||||
(gitea-fed-test
|
||||
"fed log provenance"
|
||||
(get (first (gitea/fed-log gf-B)) :origin)
|
||||
"forge-a")
|
||||
|
||||
(define gf-owners-before (len (gitea/owners gf-B)))
|
||||
(define gf-r2 (gitea/fed-receive! gf-B "forge-a" {:actor "forge-a/user:alice" :detail {:body "following up"} :object "issue:bob/hub#1" :at 51 :tags (list "repo:bob/hub") :verb "comment"}))
|
||||
|
||||
(gitea-fed-test "fed comment accepted" (get gf-r2 :materialized) "comment")
|
||||
(gitea-fed-test
|
||||
"fed comment recorded"
|
||||
(len (get (gitea/issue-get gf-B "bob" "hub" 1) :comments))
|
||||
1)
|
||||
(gitea-fed-test
|
||||
"proxy user reused"
|
||||
(len (gitea/owners gf-B))
|
||||
gf-owners-before)
|
||||
|
||||
(define gf-r3 (gitea/fed-receive! gf-B "forge-a" {:actor "forge-a/user:alice" :detail {:source "feat" :title "Fed PR" :body "take my branch" :target "main"} :object "pr:bob/hub#0" :at 52 :tags (list "repo:bob/hub") :verb "open-pr"}))
|
||||
|
||||
(gitea-fed-test "fed pr accepted" (get gf-r3 :materialized) "pr")
|
||||
(gitea-fed-test
|
||||
"fed pr author"
|
||||
(get (gitea/pr-get gf-B "bob" "hub" (get gf-r3 :number)) :author)
|
||||
"alice@forge-a")
|
||||
(gitea-fed-test
|
||||
"fed pr branches"
|
||||
(get (gitea/pr-get gf-B "bob" "hub" (get gf-r3 :number)) :source)
|
||||
"feat")
|
||||
|
||||
(gitea-fed-test
|
||||
"unknown verb still logged"
|
||||
(get (gitea/fed-receive! gf-B "forge-a" {:actor "forge-a/user:alice" :object "repo:bob/hub" :at 53 :tags (list "repo:bob/hub") :verb "star"}) :materialized)
|
||||
"none")
|
||||
(gitea-fed-test
|
||||
"comment with bad object"
|
||||
(get (gitea/fed-receive! gf-B "forge-a" {:actor "forge-a/user:alice" :object "nonsense" :at 54 :verb "comment"}) :error)
|
||||
"bad-object")
|
||||
(gitea-fed-test "fed log grows" (len (gitea/fed-log gf-B)) 5)
|
||||
|
||||
; ── inbox over the wire ──────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
gf-postB
|
||||
(fn
|
||||
(body)
|
||||
(gf-appB (dream-request "POST" "/api/ap/inbox" {} body))))
|
||||
|
||||
(gitea-fed-test
|
||||
"web inbox missing peer 400"
|
||||
(dream-status (gf-postB (dream-json-encode {:activity {}})))
|
||||
400)
|
||||
(gitea-fed-test
|
||||
"web inbox untrusted 403"
|
||||
(dream-status (gf-postB (dream-json-encode {:activity {} :peer "forge-x"})))
|
||||
403)
|
||||
(gitea-fed-test
|
||||
"web inbox trusted 200"
|
||||
(dream-status (gf-postB (dream-json-encode {:activity {:actor "forge-a/user:alice" :detail {:body "via web"} :object "issue:bob/hub#1" :at 55 :tags (list "repo:bob/hub") :verb "comment"} :peer "forge-a"})))
|
||||
200)
|
||||
(gitea-fed-test
|
||||
"web inbox materialized"
|
||||
(len (get (gitea/issue-get gf-B "bob" "hub" 1) :comments))
|
||||
2)
|
||||
|
||||
; ── mirrors (cross-instance repo follow) ─────────────────────────────
|
||||
|
||||
(define gf-m1 (gitea/mirror! gf-B "bob" "libmirror" "forge-a" "alice" "lib"))
|
||||
(gitea-fed-test "mirror clones" (get gf-m1 :owner) "bob")
|
||||
(gitea-fed-test
|
||||
"mirror branch matches upstream"
|
||||
(git/branch-get (gitea/repo-git gf-B "bob" "libmirror") "main")
|
||||
(git/branch-get gf-gA "main"))
|
||||
(gitea-fed-test
|
||||
"mirror recorded"
|
||||
(get (gitea/mirror-of gf-B "bob" "libmirror") :peer)
|
||||
"forge-a")
|
||||
(gitea-fed-test "mirrors list" (gitea/mirrors gf-B) (list "bob/libmirror"))
|
||||
|
||||
(git/checkout! gf-gA "main")
|
||||
(git/add! gf-gA "more.txt" "more\n")
|
||||
(define gf-c2 (git/commit! gf-gA {:message "c2" :time 6 :author "alice"}))
|
||||
|
||||
(gitea-fed-test
|
||||
"mirror-sync pulls new commits"
|
||||
(get (gitea/mirror-sync! gf-B "bob" "libmirror") :stored)
|
||||
3)
|
||||
(gitea-fed-test
|
||||
"mirror advanced"
|
||||
(git/branch-get (gitea/repo-git gf-B "bob" "libmirror") "main")
|
||||
gf-c2)
|
||||
|
||||
(gitea/untrust! gf-B "forge-a")
|
||||
(gitea-fed-test
|
||||
"sync of untrusted peer refused"
|
||||
(get (gitea/mirror-sync! gf-B "bob" "libmirror") :error)
|
||||
"untrusted-peer")
|
||||
(gitea-fed-test
|
||||
"mirror of untrusted peer refused"
|
||||
(get (gitea/mirror! gf-B "bob" "another" "forge-a" "alice" "lib") :error)
|
||||
"untrusted-peer")
|
||||
(gitea/trust! gf-B "forge-a")
|
||||
(gitea-fed-test
|
||||
"sync ok after re-trust"
|
||||
(get (gitea/mirror-sync! gf-B "bob" "libmirror") :stored)
|
||||
0)
|
||||
(gitea-fed-test
|
||||
"non-mirror sync refused"
|
||||
(get (gitea/mirror-sync! gf-B "bob" "hub") :error)
|
||||
"not-a-mirror")
|
||||
|
||||
; ── outbound delivery ────────────────────────────────────────────────
|
||||
|
||||
(gitea/trust! gf-A "forge-b")
|
||||
(define gf-d1 (gitea/fed-deliver! gf-A))
|
||||
|
||||
; A's public activity so far: create-repo lib, open-issue lib#1,
|
||||
; comment... none; private create/issue excluded
|
||||
(gitea-fed-test
|
||||
"deliver pushes public only"
|
||||
(get gf-d1 :delivered)
|
||||
2)
|
||||
(gitea-fed-test
|
||||
"deliver reaches trusted peers"
|
||||
(get gf-d1 :peers)
|
||||
(list "forge-b"))
|
||||
(gitea-fed-test
|
||||
"peer logged deliveries"
|
||||
(len (gitea/fed-log gf-B))
|
||||
8)
|
||||
(gitea-fed-test
|
||||
"delivered origin"
|
||||
(get
|
||||
(first
|
||||
(filter
|
||||
(fn (e) (= (get (get e :activity) :verb) "create-repo"))
|
||||
(gitea/fed-log gf-B)))
|
||||
:origin)
|
||||
"forge-a")
|
||||
|
||||
(gitea-fed-test
|
||||
"second deliver is a no-op"
|
||||
(get (gitea/fed-deliver! gf-A) :delivered)
|
||||
0)
|
||||
|
||||
(gitea/issue-comment!
|
||||
gf-A
|
||||
"alice"
|
||||
"lib"
|
||||
1
|
||||
"alice"
|
||||
"one more"
|
||||
{:at 7})
|
||||
(gitea-fed-test
|
||||
"incremental deliver"
|
||||
(get (gitea/fed-deliver! gf-A) :delivered)
|
||||
1)
|
||||
|
||||
; ── federated timeline ───────────────────────────────────────────────
|
||||
|
||||
(define gf-tl (gitea/fed-timeline gf-B nil 100))
|
||||
(gitea-fed-test
|
||||
"fed timeline mixes local and foreign"
|
||||
(>
|
||||
(len (filter (fn (a) (= (get a :origin) "forge-a")) gf-tl))
|
||||
0)
|
||||
true)
|
||||
(gitea-fed-test
|
||||
"local activities carry no origin"
|
||||
(>
|
||||
(len
|
||||
(filter
|
||||
(fn
|
||||
(a)
|
||||
(and (nil? (get a :origin)) (= (get a :verb) "create-repo")))
|
||||
gf-tl))
|
||||
0)
|
||||
true)
|
||||
|
||||
; ── actor docs over the wire ─────────────────────────────────────────
|
||||
|
||||
(define
|
||||
gf-getA
|
||||
(fn (target) (gf-appA (dream-request "GET" target {} ""))))
|
||||
|
||||
(gitea-fed-test
|
||||
"web ap user 200"
|
||||
(dream-status (gf-getA "/api/ap/users/alice"))
|
||||
200)
|
||||
(gitea-fed-test
|
||||
"web ap user type"
|
||||
(get
|
||||
(dream-json-parse (dream-resp-body (gf-getA "/api/ap/users/alice")))
|
||||
:type)
|
||||
"Person")
|
||||
(gitea-fed-test
|
||||
"web ap user missing 404"
|
||||
(dream-status (gf-getA "/api/ap/users/zeb"))
|
||||
404)
|
||||
(gitea-fed-test
|
||||
"web ap repo 200"
|
||||
(dream-status (gf-getA "/api/ap/repos/alice/lib"))
|
||||
200)
|
||||
(gitea-fed-test
|
||||
"web ap private repo hidden"
|
||||
(dream-status (gf-getA "/api/ap/repos/alice/hid"))
|
||||
404)
|
||||
(gitea-fed-test
|
||||
"web outbox 200"
|
||||
(dream-status (gf-getA "/api/ap/users/alice/outbox"))
|
||||
200)
|
||||
(gitea-fed-test
|
||||
"web fed timeline 200"
|
||||
(dream-status (gf-getA "/api/fed/timeline"))
|
||||
200)
|
||||
Reference in New Issue
Block a user