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:
61
lib/gitea/README.md
Normal file
61
lib/gitea/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# sx-gitea — a federated git forge in plain SX
|
||||
|
||||
A git forge built by **composing the x-on-sx subsystems**: every phase
|
||||
wires one more substrate onto the forge. No third-party dependencies —
|
||||
the whole stack is SX on the OCaml kernel.
|
||||
|
||||
Run the suite: `bash lib/gitea/conformance.sh` (per-suite scores in
|
||||
`scoreboard.md`). Suites are independent `sx_server` sessions; heavyweight
|
||||
substrates (Smalltalk/content, Scheme/flow, APL/feed, Haskell/search) load
|
||||
only for the suites that need them.
|
||||
|
||||
## Composition map
|
||||
|
||||
| Phase | Module | Built on |
|
||||
|-------|--------|----------|
|
||||
| 1 repo | `repo.sx` | **sx-git** (`lib/git`, native-CID object store), persist kv |
|
||||
| 2 access | `access.sx` | **acl** (datalog): repo role groups, collaborators, org teams; bearer tokens |
|
||||
| 3 wire | `wire.sx` | git-style smart HTTP: pkt-line framing, upload/receive-pack, CID-verified packs; client (`clone!`/`fetch!`/`push!`) drives any dream app fn |
|
||||
| 4 issues | `issues.sx` | **content** (Smalltalk): Markdown bodies as block documents; **relations** (datalog): derived issue graph |
|
||||
| 5 pr | `pr.sx` | **sx-git** merge-base diffs + 3-way merge; **flow** (Scheme): durable open→approval→merge lifecycle; merge queue |
|
||||
| 6 activity | `activity.sx` | **feed** (APL): timelines/dashboard; **events** (flow): durable at-least-once notifications |
|
||||
| 7 search | `search.sx` | **search** (Haskell): tf-idf ranked code/issue/PR search, batched evaluations |
|
||||
| 8 fed | `fed.sx` | ForgeFed: AP actors, trust-gated inbox with provenance + materialized federated issues/PRs, mirrors over the wire client, cursor-based delivery |
|
||||
| web | `web.sx` | **dream**: routes, auth gating (401/403/404-hides-private), route-pack registry |
|
||||
|
||||
## Architectural rules of thumb
|
||||
|
||||
- **The kv store is the source of truth.** Owners, repo records, issues,
|
||||
PRs, collaborators, teams, tokens, follows, trust, mirrors — all plain
|
||||
dicts under `gitea/...` keys on one persist backend per forge.
|
||||
Deleting a repo is a prefix purge (no ghost state on recreate).
|
||||
- **Derived, not maintained.** The acl database and the relations graph
|
||||
are *derived* from kv state and rebuilt when the derived facts change
|
||||
(cached in the forge handle) — deletions can never dangle.
|
||||
- **Instrument in the runtime.** Activity logging wraps the mutation
|
||||
verbs by redefinition (`gitea/base-*!` + wrapper), so every caller
|
||||
emits activity with zero call-site edits.
|
||||
- **Everything is testable without sockets.** A forge is a value over a
|
||||
`persist/mem-backend`; `gitea/app` is a pure request→response fn; the
|
||||
wire client federates two in-memory forges directly.
|
||||
- **Trust is re-checked, never cached.** Federation operations
|
||||
(inbox, mirror sync, delivery) consult the trust set at use time.
|
||||
|
||||
## Per-repo git stores
|
||||
|
||||
Each repo's objects/refs live in their own `git/repo-named` namespace
|
||||
`forge/<owner>/<name>` — identical content still shares CIDs, but repos
|
||||
cannot see each other's objects. All ref moves go through `ref-cas!`;
|
||||
concurrent pushes surface as `stale`/`non-fast-forward` per-ref statuses.
|
||||
|
||||
## Known limits (deliberate, documented)
|
||||
|
||||
- Wire packs carry one object per pkt line (~64KB); side-band chunking
|
||||
is a future extension (`gitea/pkt-fits?` reports it). SHA-1/packfile
|
||||
byte compat for stock git clients lives in `lib/git/{export,import}.sx`
|
||||
and is not yet wired into the HTTP endpoints.
|
||||
- Inbox activities are trust-gated but not signature-verified.
|
||||
- Reopening a PR restarts its lifecycle flow (a cancelled flow cannot
|
||||
resume); reviews survive.
|
||||
- Issue web close/reopen does not emit activity (no actor at the core
|
||||
call sites for `issue-close!`).
|
||||
449
lib/gitea/fed.sx
Normal file
449
lib/gitea/fed.sx
Normal file
@@ -0,0 +1,449 @@
|
||||
; lib/gitea/fed.sx — sx-gitea Phase 8: ForgeFed federation.
|
||||
;
|
||||
; Forges federate as fed-sx-style peers. Each forge carries an instance
|
||||
; id; users and repos project as AP actor documents (Person/Repository
|
||||
; with inbox/outbox); the outbox is the forge's activity log in an
|
||||
; AP-shaped envelope ({:actor "<instance>/user:<u>" :verb :object
|
||||
; :published}).
|
||||
;
|
||||
; Trust is the events-federation pattern: a kv set of peer ids,
|
||||
; RE-CHECKED on every operation, so revoking a peer takes effect
|
||||
; immediately. Peer transports (dream app fns) live in the forge handle's
|
||||
; runtime cache — only the trust set persists.
|
||||
;
|
||||
; The inbox (POST /api/ap/inbox, {:peer :activity}) accepts activities
|
||||
; from trusted peers only. Every accepted activity lands in a federated
|
||||
; activity log with :origin provenance; open-issue/comment/open-pr
|
||||
; activities also MATERIALIZE: the foreign author becomes a proxy user
|
||||
; "<name>@<peer>" (auto-created), and the issue/comment/PR is created
|
||||
; locally under that identity — federated issues and PRs with honest
|
||||
; provenance. fed-deliver! pushes public-repo activities (cursor-based,
|
||||
; never private ones) to every trusted peer's inbox.
|
||||
;
|
||||
; Cross-instance repo follow = mirror!: clone a trusted peer's repo over
|
||||
; the Phase 3 wire client, remember the source, and mirror-sync! to
|
||||
; fast-forward — trust re-checked on every sync.
|
||||
;
|
||||
; Requires: lib/gitea/{repo,access,web,wire,issues,pr,activity}.sx and
|
||||
; their stacks.
|
||||
|
||||
; ── instance identity ────────────────────────────────────────────────
|
||||
|
||||
(define gitea/instance-key "gitea/instance")
|
||||
|
||||
(define
|
||||
gitea/instance!
|
||||
(fn
|
||||
(forge id)
|
||||
(persist/kv-put (gitea/forge-db forge) gitea/instance-key {:id id})))
|
||||
|
||||
(define
|
||||
gitea/instance-id
|
||||
(fn
|
||||
(forge)
|
||||
(get
|
||||
(or
|
||||
(persist/kv-get (gitea/forge-db forge) gitea/instance-key)
|
||||
{})
|
||||
:id)))
|
||||
|
||||
(define
|
||||
gitea/actor-id
|
||||
(fn (forge node) (str (or (gitea/instance-id forge) "local") "/" node)))
|
||||
|
||||
; ── peers + trust ────────────────────────────────────────────────────
|
||||
|
||||
; transports are live functions — runtime registry in the forge cache
|
||||
(define
|
||||
gitea/peers-registry
|
||||
(fn
|
||||
(forge)
|
||||
(let
|
||||
((cache (get forge :cache)))
|
||||
(begin
|
||||
(if
|
||||
(and cache (nil? (get cache "peers")))
|
||||
(dict-set! cache "peers" {})
|
||||
nil)
|
||||
(or (get cache "peers") {})))))
|
||||
|
||||
(define
|
||||
gitea/peer-register!
|
||||
(fn
|
||||
(forge id app token)
|
||||
(let
|
||||
((cache (get forge :cache)))
|
||||
(begin
|
||||
(dict-set!
|
||||
cache
|
||||
"peers"
|
||||
(assoc (gitea/peers-registry forge) id {:id id :token token :app app}))
|
||||
id))))
|
||||
|
||||
(define
|
||||
gitea/peer-get
|
||||
(fn (forge id) (get (gitea/peers-registry forge) id)))
|
||||
|
||||
(define gitea/trust-key (fn (id) (str "gitea/trust/" id)))
|
||||
|
||||
(define
|
||||
gitea/trust!
|
||||
(fn
|
||||
(forge id)
|
||||
(persist/kv-put (gitea/forge-db forge) (gitea/trust-key id) {:id id})))
|
||||
|
||||
(define
|
||||
gitea/untrust!
|
||||
(fn
|
||||
(forge id)
|
||||
(persist/kv-delete (gitea/forge-db forge) (gitea/trust-key id))))
|
||||
|
||||
(define
|
||||
gitea/trusted?
|
||||
(fn
|
||||
(forge id)
|
||||
(persist/kv-has? (gitea/forge-db forge) (gitea/trust-key id))))
|
||||
|
||||
(define
|
||||
gitea/trusted-peers
|
||||
(fn
|
||||
(forge)
|
||||
(filter
|
||||
(fn (id) (gitea/trusted? forge id))
|
||||
(artdag/sort-strings (keys (gitea/peers-registry forge))))))
|
||||
|
||||
; ── AP actor documents ───────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
gitea/ap-user
|
||||
(fn
|
||||
(forge user)
|
||||
(let
|
||||
((rec (gitea/owner-get forge user)))
|
||||
(if (nil? rec) nil {:id (gitea/actor-id forge (str "user:" user)) :type (if (gitea/org? rec) "Group" "Person") :outbox (str "/api/ap/users/" user "/outbox") :preferredUsername user :inbox "/api/ap/inbox"}))))
|
||||
|
||||
(define
|
||||
gitea/ap-repo
|
||||
(fn
|
||||
(forge owner name)
|
||||
(let
|
||||
((rec (gitea/repo-get forge owner name)))
|
||||
(if (nil? rec) nil {:clone (str "/" owner "/" name "/info/refs") :name (str owner "/" name) :id (gitea/actor-id forge (str "repo:" owner "/" name)) :attributedTo (gitea/actor-id forge (str "user:" owner)) :type "Repository" :summary (get rec :description) :inbox "/api/ap/inbox"}))))
|
||||
|
||||
; AP-shaped envelope for a feed activity
|
||||
(define gitea/ap-activity (fn (forge a) {:published (get a :at) :actor (gitea/actor-id forge (str "user:" (get a :actor))) :object (get a :object) :verb (get a :verb)}))
|
||||
|
||||
(define
|
||||
gitea/ap-outbox
|
||||
(fn
|
||||
(forge user n)
|
||||
(map
|
||||
(fn (a) (gitea/ap-activity forge a))
|
||||
(gitea/user-timeline forge nil user n))))
|
||||
|
||||
; ── federated activity log (inbound, with provenance) ────────────────
|
||||
|
||||
(define gitea/fed-stream-name "gitea/fed-activity")
|
||||
|
||||
(define
|
||||
gitea/fed-log
|
||||
(fn
|
||||
(forge)
|
||||
(map
|
||||
persist/event-data
|
||||
(persist/read (gitea/forge-db forge) gitea/fed-stream-name))))
|
||||
|
||||
(define
|
||||
gitea/fed-log-append!
|
||||
(fn
|
||||
(forge origin a)
|
||||
(persist/append
|
||||
(gitea/forge-db forge)
|
||||
gitea/fed-stream-name
|
||||
(or (get a :verb) "activity")
|
||||
(or (get a :at) 0)
|
||||
{:activity a :origin origin})))
|
||||
|
||||
; local + foreign activities, newest first, foreign tagged :origin
|
||||
(define
|
||||
gitea/fed-timeline
|
||||
(fn
|
||||
(forge user n)
|
||||
(feed/items
|
||||
(feed/take
|
||||
(feed/recent
|
||||
(feed/stream
|
||||
(concat
|
||||
(feed/items
|
||||
(feed/filter
|
||||
(gitea/activity-stream forge)
|
||||
(fn (a) (gitea/act-visible? forge user a))))
|
||||
(map
|
||||
(fn (e) (assoc (get e :activity) :origin (get e :origin)))
|
||||
(gitea/fed-log forge)))))
|
||||
n))))
|
||||
|
||||
; ── inbound materialization ──────────────────────────────────────────
|
||||
|
||||
; foreign authors become local proxy users "<name>@<peer>"
|
||||
(define
|
||||
gitea/fed-user!
|
||||
(fn
|
||||
(forge name peer)
|
||||
(let
|
||||
((proxy (str name "@" peer)))
|
||||
(begin
|
||||
(if
|
||||
(gitea/owner-exists? forge proxy)
|
||||
nil
|
||||
(gitea/user-create! forge proxy))
|
||||
proxy))))
|
||||
|
||||
; a foreign activity's :actor may be "<instance>/user:<name>" or a bare
|
||||
; name — reduce it to the name
|
||||
(define
|
||||
gitea/fed-actor-name
|
||||
(fn
|
||||
(actor)
|
||||
(let
|
||||
((i (index-of (or actor "") "user:")))
|
||||
(if (< i 0) (or actor "") (substr actor (+ i 5))))))
|
||||
|
||||
; apply one trusted activity: log it, and materialize the verbs a forge
|
||||
; can host locally. => {:accepted true ...} | {:error ...}
|
||||
(define
|
||||
gitea/fed-receive!
|
||||
(fn
|
||||
(forge peer a)
|
||||
(if
|
||||
(not (gitea/trusted? forge peer))
|
||||
{:error "untrusted-peer"}
|
||||
(let
|
||||
((verb (get a :verb))
|
||||
(actor (gitea/fed-actor-name (get a :actor)))
|
||||
(node (gitea/parse-numbered-node (or (get a :object) "")))
|
||||
(detail (or (get a :detail) {})))
|
||||
(begin
|
||||
(gitea/fed-log-append! forge peer a)
|
||||
(cond
|
||||
((= verb "open-issue")
|
||||
(let
|
||||
((rp (gitea/split-full (substr (or (gitea/act-repo a) "repo:/") 5))))
|
||||
(let
|
||||
((res (gitea/issue-create! forge (get rp :owner) (get rp :name) (gitea/fed-user! forge actor peer) (or (get detail :title) "(federated issue)") (or (get detail :body) "") {:created-at (or (get a :at) 0)})))
|
||||
(if (get res :error) res {:materialized "issue" :accepted true :number (get res :number)}))))
|
||||
((= verb "comment")
|
||||
(if
|
||||
(nil? node)
|
||||
{:error "bad-object"}
|
||||
(let
|
||||
((res (gitea/issue-comment! forge (get node :owner) (get node :name) (get node :n) (gitea/fed-user! forge actor peer) (or (get detail :body) "") {:at (or (get a :at) 0)})))
|
||||
(if (get res :error) res {:materialized "comment" :accepted true}))))
|
||||
((= verb "open-pr")
|
||||
(let
|
||||
((rp (gitea/split-full (substr (or (gitea/act-repo a) "repo:/") 5))))
|
||||
(let
|
||||
((res (gitea/pr-create! forge (get rp :owner) (get rp :name) (gitea/fed-user! forge actor peer) (or (get detail :title) "(federated pr)") (get detail :source) (get detail :target) (or (get detail :body) "") {:created-at (or (get a :at) 0)})))
|
||||
(if (get res :error) res {:materialized "pr" :accepted true :number (get res :number)}))))
|
||||
(else {:materialized "none" :accepted true})))))))
|
||||
|
||||
; ── outbound delivery ────────────────────────────────────────────────
|
||||
|
||||
(define gitea/fed-cursor-key "gitea/fed-cursor")
|
||||
|
||||
; push public-repo activities after the cursor to every trusted peer's
|
||||
; inbox. => {:delivered n :peers (ids)}
|
||||
(define
|
||||
gitea/fed-deliver!
|
||||
(fn
|
||||
(forge)
|
||||
(let
|
||||
((db (gitea/forge-db forge)))
|
||||
(let
|
||||
((cursor (persist/kv-get-or db gitea/fed-cursor-key 0))
|
||||
(peers (gitea/trusted-peers forge))
|
||||
(me (or (gitea/instance-id forge) "local")))
|
||||
(let
|
||||
((events (persist/read-from db gitea/activity-stream-name (+ cursor 1))))
|
||||
(let
|
||||
((public (filter (fn (e) (gitea/act-visible? forge nil (persist/event-data e))) events)))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(e)
|
||||
(for-each
|
||||
(fn
|
||||
(pid)
|
||||
(let
|
||||
((peer (gitea/peer-get forge pid)))
|
||||
(if
|
||||
(nil? peer)
|
||||
nil
|
||||
((get peer :app)
|
||||
(dream-request
|
||||
"POST"
|
||||
"/api/ap/inbox"
|
||||
(if
|
||||
(nil? (get peer :token))
|
||||
{}
|
||||
{:authorization (str "Bearer " (get peer :token))})
|
||||
(dream-json-encode {:activity (persist/event-data e) :peer me}))))))
|
||||
peers))
|
||||
public)
|
||||
(if
|
||||
(empty? events)
|
||||
nil
|
||||
(persist/kv-put
|
||||
db
|
||||
gitea/fed-cursor-key
|
||||
(reduce (fn (acc e) (persist/event-seq e)) cursor events)))
|
||||
{:delivered (len public) :peers peers})))))))
|
||||
|
||||
; ── cross-instance repo follow (mirrors) ─────────────────────────────
|
||||
|
||||
(define
|
||||
gitea/mirror-key
|
||||
(fn (owner name) (str "gitea/mirror/" owner "/" name)))
|
||||
|
||||
(define
|
||||
gitea/peer-remote
|
||||
(fn
|
||||
(forge peer-id owner name)
|
||||
(let
|
||||
((peer (gitea/peer-get forge peer-id)))
|
||||
(if
|
||||
(nil? peer)
|
||||
nil
|
||||
(gitea/remote (get peer :app) owner name (get peer :token))))))
|
||||
|
||||
; clone a trusted peer's repo as owner/name and remember the source
|
||||
(define
|
||||
gitea/mirror!
|
||||
(fn
|
||||
(forge owner name peer-id remote-owner remote-name)
|
||||
(cond
|
||||
((not (gitea/trusted? forge peer-id)) {:error "untrusted-peer"})
|
||||
((nil? (gitea/peer-get forge peer-id)) {:error "no-such-peer"})
|
||||
(else
|
||||
(let
|
||||
((remote (gitea/peer-remote forge peer-id remote-owner remote-name)))
|
||||
(let
|
||||
((res (gitea/clone! forge owner name remote {})))
|
||||
(if
|
||||
(or (get res :error) (get res :conflict))
|
||||
res
|
||||
(begin
|
||||
(persist/kv-put
|
||||
(gitea/forge-db forge)
|
||||
(gitea/mirror-key owner name)
|
||||
{:remote-owner remote-owner :peer peer-id :remote-name remote-name})
|
||||
res))))))))
|
||||
|
||||
(define
|
||||
gitea/mirror-of
|
||||
(fn
|
||||
(forge owner name)
|
||||
(persist/kv-get (gitea/forge-db forge) (gitea/mirror-key owner name))))
|
||||
|
||||
(define
|
||||
gitea/mirrors
|
||||
(fn (forge) (gitea/names-under forge "gitea/mirror/")))
|
||||
|
||||
; re-fetch from the mirror source; trust is re-checked every sync
|
||||
(define
|
||||
gitea/mirror-sync!
|
||||
(fn
|
||||
(forge owner name)
|
||||
(let
|
||||
((m (gitea/mirror-of forge owner name)))
|
||||
(cond
|
||||
((nil? m) {:error "not-a-mirror"})
|
||||
((not (gitea/trusted? forge (get m :peer))) {:error "untrusted-peer"})
|
||||
((nil? (gitea/peer-get forge (get m :peer))) {:error "no-such-peer"})
|
||||
(else
|
||||
(gitea/fetch!
|
||||
(gitea/peer-remote
|
||||
forge
|
||||
(get m :peer)
|
||||
(get m :remote-owner)
|
||||
(get m :remote-name))
|
||||
(gitea/repo-git forge owner name)))))))
|
||||
|
||||
; ── web ──────────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
gitea/w-ap-user
|
||||
(fn
|
||||
(forge req)
|
||||
(let
|
||||
((doc (gitea/ap-user forge (dream-param req "user"))))
|
||||
(if (nil? doc) (dream-not-found) (dream-json-value doc)))))
|
||||
|
||||
(define
|
||||
gitea/w-ap-repo
|
||||
(fn
|
||||
(forge req)
|
||||
(let
|
||||
((owner (dream-param req "owner")) (name (dream-param req "name")))
|
||||
(if
|
||||
(not (gitea/w-readable? forge req owner name))
|
||||
(dream-not-found)
|
||||
(dream-json-value (gitea/ap-repo forge owner name))))))
|
||||
|
||||
(define
|
||||
gitea/w-ap-outbox
|
||||
(fn
|
||||
(forge req)
|
||||
(let
|
||||
((user (dream-param req "user")))
|
||||
(if
|
||||
(not (gitea/owner-exists? forge user))
|
||||
(dream-not-found)
|
||||
(dream-json-value (gitea/ap-outbox forge user 50))))))
|
||||
|
||||
(define
|
||||
gitea/w-ap-inbox
|
||||
(fn
|
||||
(forge req)
|
||||
(let
|
||||
((body (dream-json-body req)))
|
||||
(let
|
||||
((peer (get body :peer)))
|
||||
(cond
|
||||
((nil? peer) (gitea/w-json-status 400 {:error "missing-peer"}))
|
||||
((not (gitea/trusted? forge peer)) (gitea/w-forbidden))
|
||||
(else
|
||||
(let
|
||||
((res (gitea/fed-receive! forge peer (or (get body :activity) {}))))
|
||||
(if
|
||||
(get res :error)
|
||||
(gitea/w-json-status 400 {:error (get res :error)})
|
||||
(dream-json-value res)))))))))
|
||||
|
||||
(define
|
||||
gitea/w-fed-timeline
|
||||
(fn
|
||||
(forge req)
|
||||
(dream-json-value
|
||||
(gitea/fed-timeline forge (gitea/w-user forge req) 50))))
|
||||
|
||||
(define
|
||||
gitea/fed-routes
|
||||
(fn
|
||||
(forge)
|
||||
(list
|
||||
(dream-get
|
||||
"/api/ap/users/:user"
|
||||
(fn (req) (gitea/w-ap-user forge req)))
|
||||
(dream-get
|
||||
"/api/ap/users/:user/outbox"
|
||||
(fn (req) (gitea/w-ap-outbox forge req)))
|
||||
(dream-get
|
||||
"/api/ap/repos/:owner/:name"
|
||||
(fn (req) (gitea/w-ap-repo forge req)))
|
||||
(dream-post "/api/ap/inbox" (fn (req) (gitea/w-ap-inbox forge req)))
|
||||
(dream-get
|
||||
"/api/fed/timeline"
|
||||
(fn (req) (gitea/w-fed-timeline forge req))))))
|
||||
|
||||
(set! gitea/route-packs (append gitea/route-packs (list gitea/fed-routes)))
|
||||
@@ -5,9 +5,11 @@
|
||||
"wire": {"pass": 78, "fail": 0},
|
||||
"issues": {"pass": 88, "fail": 0},
|
||||
"pr": {"pass": 100, "fail": 0},
|
||||
"activity": {"pass": 60, "fail": 0}
|
||||
"activity": {"pass": 60, "fail": 0},
|
||||
"search": {"pass": 35, "fail": 0},
|
||||
"fed": {"pass": 60, "fail": 0}
|
||||
},
|
||||
"total_pass": 520,
|
||||
"total_pass": 615,
|
||||
"total_fail": 0,
|
||||
"total": 520
|
||||
"total": 615
|
||||
}
|
||||
|
||||
@@ -10,4 +10,6 @@ _Generated by `lib/gitea/conformance.sh`_
|
||||
| issues | 88 | 0 | 88 |
|
||||
| pr | 100 | 0 | 100 |
|
||||
| activity | 60 | 0 | 60 |
|
||||
| **Total** | **520** | **0** | **520** |
|
||||
| search | 35 | 0 | 35 |
|
||||
| fed | 60 | 0 | 60 |
|
||||
| **Total** | **615** | **0** | **615** |
|
||||
|
||||
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