; lib/gitea/tests/activity.sx — Phase 6: instrumented activity log, feed ; timelines with visibility, follows + dashboard, durable notifications ; into per-user inboxes, and the activity web routes. (st-bootstrap-classes!) (content/bootstrap!) (content-bootstrap-markdown!) (content-bootstrap-table!) (define gitea-act-pass 0) (define gitea-act-fail 0) (define gitea-act-fails (list)) (define gitea-act-test (fn (name actual expected) (if (= actual expected) (set! gitea-act-pass (+ gitea-act-pass 1)) (begin (set! gitea-act-fail (+ gitea-act-fail 1)) (set! gitea-act-fails (append gitea-act-fails (list {:name name :expected (inspect expected) :actual (inspect actual)}))))))) ; ── setup + instrumented history ───────────────────────────────────── ; activity ledger (seq/at/actor/verb): ; 1 @5 alice create-repo repo:alice/proj (public) ; 2 @6 alice create-repo repo:alice/sec (private) ; 3 @10 alice open-issue issue:alice/proj#1 ; 4 @11 bob comment issue:alice/proj#1 ; 5 @12 bob open-pr pr:alice/proj#2 ; 6 @13 carol review pr:alice/proj#2 ; 7 @14 alice merge-pr pr:alice/proj#2 ; 8 @20 alice open-issue issue:alice/sec#1 (private) ; 9 @21 alice open-issue issue:alice/proj#3 (assignee bob) (define ga-db (persist/mem-backend)) (define ga-forge (gitea/forge ga-db)) (gitea/user-create! ga-forge "alice") (gitea/user-create! ga-forge "bob") (gitea/user-create! ga-forge "carol") (gitea/user-create! ga-forge "eve") (gitea/token-create! ga-forge "alice" "tok-a") (gitea/token-create! ga-forge "bob" "tok-b") (gitea/token-create! ga-forge "carol" "tok-c") (gitea/repo-create! ga-forge "alice" "proj" {:created-at 5}) (gitea/repo-create! ga-forge "alice" "sec" {:created-at 6 :visibility "private"}) (gitea/collab-add! ga-forge "alice" "sec" "bob" "read") (define ga-g (gitea/repo-git ga-forge "alice" "proj")) (git/add! ga-g "README.md" "base\n") (git/commit! ga-g {:message "c1" :time 1 :author "alice"}) (git/branch! ga-g "feat") (git/checkout! ga-g "feat") (git/add! ga-g "f.txt" "feature\n") (git/commit! ga-g {:message "c2" :time 2 :author "bob"}) (git/checkout! ga-g "main") (gitea/issue-create! ga-forge "alice" "proj" "alice" "First issue" "body" {:created-at 10}) (gitea/issue-comment! ga-forge "alice" "proj" 1 "bob" "a comment" {:at 11}) (gitea/pr-create! ga-forge "alice" "proj" "bob" "Feature" "feat" "main" "" {:created-at 12}) (gitea/pr-review! ga-forge "alice" "proj" 2 "carol" "approve" "lgtm" {:at 13}) (gitea/pr-merge! ga-forge "alice" "proj" 2 "alice" {:time 14}) (gitea/issue-create! ga-forge "alice" "sec" "alice" "Secret issue" "" {:created-at 20}) (gitea/issue-create! ga-forge "alice" "proj" "alice" "Assigned issue" "" {:assignees (list "bob") :created-at 21}) (gitea-act-test "activity count" (gitea/activity-count ga-forge) 9) ; failed mutations emit nothing (gitea/issue-create! ga-forge "alice" "none" "alice" "x" "" {}) (gitea/pr-review! ga-forge "alice" "proj" 2 "bob" "approve" "" {}) (gitea-act-test "errors emit no activity" (gitea/activity-count ga-forge) 9) ; ── timelines ──────────────────────────────────────────────────────── (gitea-act-test "anon timeline hides private" (len (gitea/timeline ga-forge nil 50)) 7) (gitea-act-test "owner timeline sees all" (len (gitea/timeline ga-forge "alice" 50)) 9) (gitea-act-test "timeline newest first" (get (first (gitea/timeline ga-forge nil 50)) :verb) "open-issue") (gitea-act-test "timeline take" (len (gitea/timeline ga-forge "alice" 2)) 2) (gitea-act-test "repo timeline proj" (len (gitea/repo-timeline ga-forge "alice" "proj" 50)) 7) (gitea-act-test "repo timeline sec" (len (gitea/repo-timeline ga-forge "alice" "sec" 50)) 2) (gitea-act-test "repo timeline order" (get (first (gitea/repo-timeline ga-forge "alice" "proj" 50)) :at) 21) (gitea-act-test "user timeline bob" (len (gitea/user-timeline ga-forge nil "bob" 50)) 2) (gitea-act-test "user timeline bob order" (get (first (gitea/user-timeline ga-forge nil "bob" 50)) :verb) "open-pr") (gitea-act-test "user timeline alice anon" (len (gitea/user-timeline ga-forge nil "alice" 50)) 4) (gitea-act-test "user timeline alice as collab" (len (gitea/user-timeline ga-forge "bob" "alice" 50)) 6) ; ── follows + dashboard ────────────────────────────────────────────── (gitea/follow! ga-forge "carol" "user:alice") (gitea-act-test "follows list" (gitea/follows ga-forge "carol") (list "user:alice")) (gitea-act-test "follow unknown follower" (get (gitea/follow! ga-forge "zeb" "user:alice") :error) "no-such-user") (gitea-act-test "follow unknown user target" (get (gitea/follow! ga-forge "carol" "user:zeb") :error) "no-such-target") (gitea-act-test "follow unknown repo target" (get (gitea/follow! ga-forge "carol" "repo:alice/none") :error) "no-such-target") (gitea-act-test "follow malformed target" (get (gitea/follow! ga-forge "carol" "alice") :error) "no-such-target") (gitea-act-test "dashboard follows a user" (len (gitea/dashboard ga-forge "carol" 50)) 4) (gitea-act-test "dashboard first actor" (get (first (gitea/dashboard ga-forge "carol" 50)) :actor) "alice") (gitea/follow! ga-forge "bob" "repo:alice/proj") (gitea-act-test "dashboard follows a repo, excludes own actions" (len (gitea/dashboard ga-forge "bob" 50)) 5) (gitea-act-test "unfollow" (gitea/unfollow! ga-forge "carol" "user:alice") true) (gitea-act-test "dashboard after unfollow" (len (gitea/dashboard ga-forge "carol" 50)) 0) (gitea-act-test "unfollow twice" (gitea/unfollow! ga-forge "carol" "user:alice") false) ; ── notifications ──────────────────────────────────────────────────── (gitea-act-test "recipients of a comment" (gitea/notify-recipients ga-forge (feed/activity "bob" "comment" "issue:alice/proj#1" 11 (list "repo:alice/proj"))) (list "alice")) (gitea-act-test "recipients of a review" (gitea/notify-recipients ga-forge (feed/activity "carol" "review" "pr:alice/proj#2" 13 (list "repo:alice/proj"))) (list "bob")) (gitea-act-test "recipients exclude the actor" (gitea/notify-recipients ga-forge (feed/activity "alice" "comment" "issue:alice/proj#1" 30 (list "repo:alice/proj"))) (list "bob")) (define ga-pend (gitea/pending-notifications ga-forge)) (gitea-act-test "pending message count" (len (get ga-pend :messages)) 4) (gitea-act-test "pending last seq" (get ga-pend :last-seq) 9) (define ga-out1 (gitea/notify! ga-forge)) (gitea-act-test "notify delivers all" (len (filter (fn (o) (= (first o) "delivered")) ga-out1)) 4) (gitea-act-test "inbox alice" (gitea/inbox-count ga-forge "alice") 1) (gitea-act-test "inbox alice body" (get (first (gitea/inbox ga-forge "alice")) :body) "bob comment issue:alice/proj#1") (gitea-act-test "inbox bob" (gitea/inbox-count ga-forge "bob") 3) (gitea-act-test "notify rerun is a no-op" (gitea/notify! ga-forge) (list)) (gitea-act-test "inboxes stable after rerun" (gitea/inbox-count ga-forge "bob") 3) ; a fresh comment (carol) notifies the author and the other commenter (gitea/issue-comment! ga-forge "alice" "proj" 1 "carol" "me too" {:at 30}) (define ga-out2 (gitea/notify! ga-forge)) (gitea-act-test "incremental delivery" (len ga-out2) 2) (gitea-act-test "inbox alice grows" (gitea/inbox-count ga-forge "alice") 2) (gitea-act-test "inbox bob grows" (gitea/inbox-count ga-forge "bob") 4) ; ── web ────────────────────────────────────────────────────────────── (define ga-app (gitea/app ga-forge)) (define ga-hdr (fn (tok) (if (nil? tok) {} {:authorization (str "Bearer " tok)}))) (define ga-get (fn (target tok) (ga-app (dream-request "GET" target (ga-hdr tok) "")))) (define ga-post (fn (target tok body) (ga-app (dream-request "POST" target (ga-hdr tok) body)))) (define ga-del (fn (target tok) (ga-app (dream-request "DELETE" target (ga-hdr tok) "")))) (gitea-act-test "activity page 200" (dream-status (ga-get "/activity" nil)) 200) (gitea-act-test "activity page shows merges" (contains? (dream-resp-body (ga-get "/activity" nil)) "merge-pr") true) (gitea-act-test "activity page hides private from anon" (contains? (dream-resp-body (ga-get "/activity" nil)) "alice/sec") false) (gitea-act-test "activity page shows private to owner" (contains? (dream-resp-body (ga-get "/activity" "tok-a")) "alice/sec") true) (gitea-act-test "repo activity page 200" (dream-status (ga-get "/alice/proj/activity" nil)) 200) (gitea-act-test "repo activity shows pr open" (contains? (dream-resp-body (ga-get "/alice/proj/activity" nil)) "open-pr") true) (gitea-act-test "private repo activity anon 404" (dream-status (ga-get "/alice/sec/activity" nil)) 404) (gitea-act-test "private repo activity collab 200" (dream-status (ga-get "/alice/sec/activity" "tok-b")) 200) (gitea-act-test "api user activity len" (len (dream-json-parse (dream-resp-body (ga-get "/api/users/bob/activity" nil)))) 2) (gitea-act-test "api user activity unknown 404" (dream-status (ga-get "/api/users/zeb/activity" nil)) 404) (gitea-act-test "api dashboard anon 401" (dream-status (ga-get "/api/dashboard" nil)) 401) (gitea-act-test "api dashboard bob" (len (dream-json-parse (dream-resp-body (ga-get "/api/dashboard" "tok-b")))) 6) (gitea-act-test "api follow anon 401" (dream-status (ga-post "/api/follow" nil (dream-json-encode {:target "user:bob"}))) 401) (gitea-act-test "api follow 200" (dream-status (ga-post "/api/follow" "tok-c" (dream-json-encode {:target "user:bob"}))) 200) (gitea-act-test "api follow recorded" (gitea/follows ga-forge "carol") (list "user:bob")) (gitea-act-test "api follow bad target 400" (dream-status (ga-post "/api/follow" "tok-c" (dream-json-encode {:target "nope"}))) 400) (gitea-act-test "api unfollow 200" (dream-status (ga-del "/api/follow/user:bob" "tok-c")) 200) (gitea-act-test "api unfollow missing 404" (dream-status (ga-del "/api/follow/user:bob" "tok-c")) 404) (gitea-act-test "api notifications anon 401" (dream-status (ga-get "/api/notifications" nil)) 401) (gitea-act-test "api notifications bodies" (contains? (dream-json-parse (dream-resp-body (ga-get "/api/notifications" "tok-a"))) "bob comment issue:alice/proj#1") true) (gitea-act-test "api notify run anon 401" (dream-status (ga-post "/api/notify/run" nil "{}")) 401) (gitea-act-test "api notify run 200" (dream-status (ga-post "/api/notify/run" "tok-a" "{}")) 200)