constitution · epochs · watch · epoch 3

comparison

c_4a5c84c0a37b (tommy-mor) vs c_597d3f736194 (tommy-mor)

download prompt · raw event · cmp_df12d833d79009

council reasoning

openai/gpt-chat-latest · winner B · 3:2 · permalink

Side B fixes multiple concrete failures in the OAuth test infrastructure that were breaking end-to-end authentication: it corrects request parsing (`getRequestBody` instead of `getInputStream`, regex-based query splitting), prevents null-related crashes, fixes redirect handling, and wraps mock handlers to return diagnostics instead of crashing. Side A improves URL generation by using display paths and substantially strengthens the vote-pool test with full pairwise coverage and ranking assertions, but much of its patch is expanded test logic rather than core functionality, whereas Side B restores a foundational test/auth flow used across the project.

openai/gpt-5.3-chat · winner A · 3:2 · permalink

A changes production code to use `display_path()` in vote URLs instead of full storage URLs, aligning links with user-visible paths, and adds a comprehensive test that exercises all 45 pairwise votes and validates final ranking correctness. B primarily fixes test infrastructure (query parsing, request body reading, null handling, and mock server robustness) to restore OAuth E2E tests, which is valuable but less impactful than A’s user-facing behavior change plus stronger correctness guarantees.

openai/gpt-5.2-chat · winner B · 3:2 · permalink

Side B fixes concrete crashes in the OAuth test infrastructure: it corrects query parsing (`str/split pair #"=" 2`), reads POST bodies from `.getRequestBody`, guards `parse-token-user` against nil, ensures redirects send a 0-length body instead of -1, and wraps the HttpHandler in a try/catch to prevent server crashes—restoring broken Playwright login flows. Side A improves URL display paths and strengthens a vote-pool test to cover all 45 pairs with a ranking assertion, but B addresses fundamental test auth failures that blocked E2E flows.

~anthropic/claude-sonnet-latest · winner B · 6:4 · permalink

Side B fixes genuine crashing bugs in shared OAuth test infrastructure (str/split called with a string instead of a regex, .getInputStream vs the correct .getRequestBody, missing nil-guards, plus a catch-all error handler) that were breaking all Playwright-based E2E auth flows—this is a real, broadly-impactful bugfix. Side A improves href display and rewrites one test to be more thorough, which is useful but narrower in scope and affects only vote-pool link rendering and one test's coverage.

~x-ai/grok-latest · winner B · 3:2 · permalink

B fixes concrete test-infra bugs (Clojure `str/split` on "=" vs #"=", `getRequestBody` vs `getInputStream`, null-safe token/state handling, 302 response length) that had broken OAuth mocks and thus all Playwright auth E2E flows. A’s `display_path` href change is a real prod consistency fix and the 45-pair ranking assertion is stronger coverage, but it extends one feature path rather than restoring foundational auth testing across the suite.

sides

A — c_4a5c84c0a37b (tommy-mor)

message

[0728c06a] Vote pool: use display_path in hrefs; test all 45 pairs + assert ranking.

- vote_compare_href and vote_pool_href now encode ~/… and -/… as their
  short display forms (not the full https://slug.social/… storage URL),
  matching what users see in the item display and DSL.
- Rewrite browser_vote_pool test to vote all C(10,2)=45 pairs in the
  pool, always preferring the alphabetically-earlier letter, then query
  GetGardenRank and assert the 10 items form one component ranked a→j.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff preview

diff --git a/server/src/html/garden/vote.rs b/server/src/html/garden/vote.rs
index 2682cfcbd2f834b56459a02831aa225cffe67c58..d0ec78cd675eae284d056fb3b8eaf5cc853d6263 100644
--- a/server/src/html/garden/vote.rs
+++ b/server/src/html/garden/vote.rs
@@ -234,8 +234,10 @@ pub(super) fn vote_compare_href(
     thread_override: Option<&str>,
     pool: Option<&ItemId>,
 ) -> String {
-    let left_q = urlencoding::encode(left.as_str());
-    let right_q = urlencoding::encode(right.as_str());
+    let left_dp = left.display_path();
+    let right_dp = right.display_path();
+    let left_q = urlencoding::encode(&left_dp);
+    let right_q = urlencoding::encode(&right_dp);
     let mut base = format!(
         "{}/vote?left={}&right={}",
         nav.room_path_prefix_for_vote_compare(),
@@ -246,16 +248,20 @@ pub(super) fn vote_compare_href(
         base = format!("{}&thread={}", base, urlencoding::encode(t));
     }
     if let Some(p) = pool {
-        base = format!("{}&pool={}", base, urlencoding::encode(p.as_str()));
+        let pool_dp = p.display_path();
+        base = format!("{}&pool={}", base, urlencoding::encode(&pool_dp));
     }
     base
 }
 
 pub(super) fn vote_pool_href(nav: &ThreadNav, pool_item_str: &str) -> String {
+    let display = ItemId::parse(pool_item_str)
+        .map(|i| i.display_path())
+        .unwrap_or_else(|| pool_item_str.to_string());
     format!(
         "{}/vote?pool={}",
         nav.room_path_prefix_for_vote_compare(),
-        urlencoding::encode(pool_item_str)
+        urlencoding::encode(&display)
     )
 }
 
diff --git a/test/browser_vote_pool.clj b/test/browser_vote_pool.clj
index d51f05db47dc3cb013e56e65f3a1edfbbcbd96ab..23d0bd80b02bd8b1b48853454bed02793296550e 100644
--- a/test/browser_vote_pool.clj
+++ b/test/browser_vote_pool.clj
@@ -1,6 +1,8 @@
 (ns test.browser-vote-pool
-  "Pool-scoped voting: seed ~/pool/a-j, enter via /vote?pool=~/pool, follow
-   the vote → next-pair → vote sequence until no next pair or 15 iterations."
+  "Pool-scoped voting: seed ~/pool/a-j (10 letters), follow the
+   vote → next-pair sequence for all C(10,2)=45 pairs voting the
+   alphabetically-earlier item each time, then assert the garden
+   ranking is a…j in order."
   (:require [babashka.fs :as fs]
             [cheshire.core :as json]
             [clojure.string :as str]
@@ -11,26 +13,38 @@
             [test.common :as common]
             [test.oauth :as oauth]))
 
+(def letters ["a" "b" "c" "d" "e" "f" "g" "h" "i" "j"])
+(def total-pairs (/ (* (count letters) (dec (count letters))) 2)) ; C(10,2) = 45
+
 (defn- wait-for-text [pg selector expected timeout-ms]
   (let [deadline (+ (System/currentTimeMillis) timeout-ms)]
     (loop []
-      (let [text (locator/text-content (page/locator pg selector))]
+      (let [text (try (locator/text-content (page/locator pg selector)) (catch Exception _ nil))]
         (if (and (string? text) (str/includes? text expected))
           true
           (if (< (System/currentTimeMillis) deadline)
-            (do (Thread/sleep 200) (recur))
+            (do (Thread/sleep 150) (recur))
             false))))))
 
 (defn- element-text [pg selector]
-  (try (locator/text-content (page/locator pg selector)) (catch Exception _ nil)))
+  (try (locator/text-content (page/locator pg selector)) (catch Exception _ "")))
 
 (defn- enc [^String s]
   (java.net.URLEncoder/encode s "UTF-8"))
 
-(def letters ["a" "b" "c" "d" "e" "f" "g" "h" "i" "j"])
+;; Extract the terminal path segment, e.g. "~/pool/c" → "c".
+(defn- leaf [path] (last (str/split path #"/")))
+
+;; Set the hidden ratio inputs so the alphabetically-earlier item wins.
+(defn- set-ratio! [pg left-text right-text]
+  (let [[rl rr] (if (neg? (compare (leaf left-text) (leaf right-text)))
+                  [100 0]   ; left is earlier → prefer left
+                  [0 100])] ; right is earlier → prefer right
+    (page/evaluate pg (str "document.getElementById('vote-ratio-left').value='" rl "'"))
+    (page/evaluate pg (str "document.getElementById('vote-ratio-right').value='" rr "'"))))
 
 (defn vote-pool-flow! []
-  (println "\n━━━ browser vote pool (/vote?pool= seeds + follow next-pair sequence) ━━━\n")
+  (println (str "\n━━━ browser vote pool (all " total-pairs " pairs → sorted ranking) ━━━\n"))
 
   (common/letlocals
    (bind build (common/run-cargo-build-release! ["slugsocial-server"]))
@@ -54,7 +68,6 @@
 
      (let [alice-token (oauth/fetch-bearer-token! base-url :username "alice")
            thread-tag  "browser-vote-pool"
-           ;; seed ~/pool/a through ~/pool/j as items with bodies
            item-lines  (str/join "\n"
                                  (map (fn [l] (str "~/pool/" l " {" l "}")) letters))
            raw         (str "# " thread-tag "\n\n~/pool {root}\n" item-lines "\n")
@@ -77,51 +90,57 @@
                (page/navigate pg (str base-url "/login"))
                (is (wait-for-text pg "body" "@alice" 15000) "alice session after login")
 
-               ;; Enter via pool URL — page picks first pair automatically.
                (page/navigate pg pool-url)
                (is (wait-for-text pg "body.view-vote-compare" "compare" 15000)
                    "pool entry: vote compare page loads")
 
-               ;; Verify the initial pair is within the pool.
-               (let [pair-text (element-text pg ".vote-compare-pair")]
-                 (is (and (string? pair-text) (str/includes? pair-text "~/pool/"))
-                     (str "initial pair is within ~/pool: " pair-text)))
-
-               ;; Follow vote → next-pair sequence up to 15 iterations.
-               (let [votes-cast
-                     (loop [i 0]
-                       (if (>= i 15)
-                         i
-                         (let [explanation (str "pool vote " i " reason")]
-                           (locator/fill (page/locator pg "#vote-explain") explanation)
-                           (locator/click (page/locator pg "#vote-compare-form button[type=submit]"))
-                           ;; Wait for edge history morph confirming the vote landed.
-                           (if-not (wait-for-text pg "ul.vote-edge-history" explanation 20000)
-                             (do (println "  vote" i "history morph timed out — stopping")
-                                 i)
-                             (let [has-next (wait-for-text pg "[data-testid=\"vote-next-pair\"]"
-                                                           "next pair" 8000)]
-                               (if-not has-next
-                                 ;; "no next pair" — pool exhausted.
-                                 (do (println "  no next pair after vote" i " — pool exhausted")
-                                     (inc i))
-                                 (do
-                                   ;; Verify the pair on this page is within the pool before advancing.
-                                   (let [pt (element-text pg ".vote-compare-pair")]
-                                     (is (and (string? pt) (str/includes? pt "~/pool/"))
-                                         (str "pair at vote " i " is within ~/pool: " pt)))
-                                   (locator/click (page/locator pg "[data-testid=\"vote-next-pair\"]"))
-                                   ;; Wait for next pair to load.
-                                   (wait-for-text pg "body.view-vote-compare" "compare" 10000)
-                                   (recur (inc i)))))))))]
-
-                 (is (>= votes-cast 1) (str "cast at least 1 vote, got: " votes-cast))
-                 (println (str "  pool voting sequence complete: " votes-cast " vote(s) cast")))
-
-               ;; After the sequence, the current page is still a pool-scoped vote page.
-               (let [url (page/url pg)]
-                 (is (str/includes? (or url "") "/vote")
-                     (str "still on /vote after sequence: " url))))))))
+               ;; Vote all 45 pairs, always preferring the alphabetically-earlier item.
+               (loop [votes-cast 0]
+                 (when (< votes-cast total-pairs)
+                   (let [left-text  (element-text pg ".vote-compare-left code")
+                         right-text (element-text pg ".vote-compare-right code")]
+                     (is (str/includes? left-text "~/pool/")
+                         (str "vote " votes-cast ": left is in pool: " left-text))
+                     (is (str/includes? right-text "~/pool/")
+                         (str "vote " votes-cast ": right is in pool: " right-text))
+                     (set-ratio! pg left-text right-text)
+                     (let [winner (if (neg? (compare (leaf left-text) (leaf right-text)))
+                                    (leaf left-text) (leaf right-text))]
+                       (locator/fill (page/locator pg "#vote-explain")
+                                     (str "prefer " winner)))
+                     (locator/click (page/locator pg "#vote-compare-form button[type=submit]"))
+                     (is (wait-for-text pg "ul.vote-edge-history" "prefer " 20000)
+                         (str "vote " votes-cast " appears in edge history"))
+                     (when (< (inc votes-cast) total-pairs)
+                       (is (wait-for-text pg "[data-testid=\"vote-next-pair\"]" "next pair" 8000)
+                           (str "next pair available after vote " votes-cast))
+                       (locator/click (page/locator pg "[data-testid=\"vote-next-pair\"]"))
+                       (is (wait-for-text pg "body.view-vote-compare" "compare" 10000)
+                           (str "vote page loaded for pair " (inc votes-cast))))
+                     (recur (inc votes-cast)))))
+
+               (println (str "  cast all " total-pairs " votes"))
+
+               ;; Query the ranking via RPC and assert alphabetical order.
+               (let [rank-resp  (oauth/http-post-json
+                                 (str base-url "/api/v0/rpc")
+                                 [{"GetGardenRank" {"room"        "public"
+                                                    "parent_path" "~/pool"}}]
+                                 :headers {"Authorization" (str "Bearer " alice-token)})
+                     rank-json  (json/parse-string (:body rank-resp) true)
+                     result     (get-in rank-json [:results 0 :result :GardenRank])
+                     components (:components result)
+                     unranked   (:unranked_items result)
+                     ranked     (mapv :item (mapcat :ranking components))
+                     ranked-leaves (mapv #(last (str/split % #"[/~]+")) ranked)]
+                 (is (= 1 (count components))
+                     (str "all 10 items form one connected component (got " (count components) ")"))
+                 (is (empty? unranked)
+                     (str "no unranked items (got " (count unranked) ")"))
+                 (is (= 10 (count ranked))
+                     (str "10 items ranked (got " (count ranked) ")"))
+                 (is (= letters ranked-leaves)
+                     (str "ranking is alphabetical a→j (got " ranked-leaves ")"))))))))
 
      (finally
        (when-some [s @!server] (common/kill-server s))

download full diff A

B — c_597d3f736194 (tommy-mor)

message

[075d4d37] Fix OAuth test mocks so Clojure E2E auth flows work again.

HttpServer handlers were crashing on query parsing and token POSTs, which broke Playwright login; also read alias/history via real CSS selectors.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/test/auth_login.clj b/test/auth_login.clj
index 6f2f00d7aa7340e63d1ac465b0a2234cec7a983f..aa63b66ccb2471b710ba3d168d0461252b39fc10 100644
--- a/test/auth_login.clj
+++ b/test/auth_login.clj
@@ -14,27 +14,27 @@
 (defn- type-alias! [pg text]
   (page/evaluate pg
                  (.replace
-                  "(() => { const i = document.getElementById('alias-input'); const f = document.getElementById('alias-check-form'); if (!i || !f) return;
+                  "(() => { const i = document.getElementById('alias-input'); const f = document.getElementById('alias-check-form'); if (!i || !f) return Promise.resolve('missing-form');
                     i.value = __TEXT__;
                     const cf = document.getElementById('alias-claim-field'); if (cf) cf.value = i.value;
                     return fetch(f.action, { method: 'POST', credentials: 'same-origin',
                       headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                       body: new URLSearchParams(new FormData(f)).toString() })
                       .then(function (r) { return r.text(); })
-                      .then(function (t) { eval(t); }); })()"
+                      .then(function (t) { eval(t); return document.getElementById('alias-status')?.textContent || ''; }); })()"
                   "__TEXT__"
-                  (pr-str text)))
-  (Thread/sleep 400))
+                  (pr-str text))))
 
-(defn- element-text [pg test-id]
+(defn- element-text [pg selector]
   (let [raw (page/evaluate pg
-                           (str "document.querySelector('[data-testid=\"" test-id "\"]')?.textContent || ''"))]
+                           (str "document.querySelector(" (pr-str selector) ")?.textContent || ''"))]
     (when (string? raw) (str/trim raw))))
 
 (defn- wait-for-text [pg test-id text timeout-ms]
-  (let [deadline (+ (System/currentTimeMillis) timeout-ms)]
+  (let [deadline (+ (System/currentTimeMillis) timeout-ms)
+        selector (str "[data-testid=\"" test-id "\"]")]
     (loop []
-      (let [got (or (element-text pg test-id) "")]
+      (let [got (or (element-text pg selector) "")]
         (cond
           (= got text) got
           (< (System/currentTimeMillis) deadline) (do (Thread/sleep 200) (recur))
diff --git a/test/support/mock_oauth.clj b/test/support/mock_oauth.clj
index 5ba7e9be3cf6d2226648e9609a09ed45f306930f..333fe08d56cb06bac2a338cad1c73894d54c4495 100644
--- a/test/support/mock_oauth.clj
+++ b/test/support/mock_oauth.clj
@@ -7,7 +7,7 @@
 (defn- query-param [query key]
   (when query
     (some (fn [pair]
-            (let [[k v] (str/split pair "=" 2)]
+            (let [[k v] (str/split pair #"=" 2)]
               (when (= k key)
                 (URLDecoder/decode (or v "") "UTF-8"))))
           (str/split query #"&"))))
@@ -31,11 +31,11 @@
 
 (defn- send-redirect [^HttpExchange ex location]
   (.set (.getResponseHeaders ex) "Location" location)
-  (.sendResponseHeaders ex 302 -1)
+  (.sendResponseHeaders ex 302 0)
   (.close (.getResponseBody ex)))
 
 (defn- read-form [^HttpExchange ex]
-  (let [body (slurp (.getInputStream ex))]
+  (let [body (slurp (.getRequestBody ex))]
     {:code (query-param body "code")
      :grant (query-param body "grant_type")}))
 
@@ -45,7 +45,7 @@
           (str/replace #"^[Bb]earer " "")))
 
 (defn- parse-token-user [token]
-  (when (str/starts-with? token "mock:")
+  (when (and token (str/starts-with? token "mock:"))
     (parse-mock-user (subs token 5))))
 
 (defn- authorize-redirect [exchange query]
@@ -55,7 +55,7 @@
         user (parse-mock-user mock-user)
         code (str "mock:" (:id user) ":" (:login user))
         loc (str redirect-uri "?code=" (java.net.URLEncoder/encode code "UTF-8")
-                 "&state=" (java.net.URLEncoder/encode state "UTF-8"))]
+                 "&state=" (java.net.URLEncoder/encode (or state "") "UTF-8"))]
     (send-redirect exchange loc)))
 
 (defn start-mock-oauth
@@ -65,49 +65,56 @@
         handler
         (proxy [HttpHandler] []
           (handle [^HttpExchange exchange]
-            (let [uri (.getRequestURI exchange)
-                  path (.getPath uri)
-                  query (.getQuery uri)
-                  method (.getRequestMethod exchange)]
-              (cond
-                ;; GitHub authorize
-                (str/ends-with? path "/login/oauth/authorize")
-                (authorize-redirect exchange query)
+            (try
+              (let [uri (.getRequestURI exchange)
+                    path (.getPath uri)
+                    query (.getQuery uri)
+                    method (.getRequestMethod exchange)]
+                (cond
+                  ;; GitHub authorize
+                  (str/ends-with? path "/login/oauth/authorize")
+                  (authorize-redirect exchange query)
 
-                ;; Reddit authorize
-                (str/ends-with? path "/api/v1/authorize")
-                (authorize-redirect exchange query)
+                  ;; Reddit authorize
+                  (str/ends-with? path "/api/v1/authorize")
+                  (authorize-redirect exchange query)
 
-                ;; GitHub token
-                (and (= method "POST") (str/ends-with? path "/login/oauth/access_token"))
-                (let [code (or (:code (read-form exchange)) "mock:1002:newbie")]
-                  (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\"}")))
+                  ;; GitHub token
+                  (and (= method "POST") (str/ends-with? path "/login/oauth/access_token"))
+                  (let [code (or (:code (read-form exchange)) "mock:1002:newbie")]
+                    (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\"}")))
 
-                ;; Reddit token (client_credentials for import + authorization_code for login)
-                (and (= method "POST") (str/ends-with? path "/api/v1/access_token"))
-                (let [form (read-form exchange)
-                      grant (or (:grant form) "")
-                      code (or (:code form) "mock:t2_test:redditor")]
-                  (if (= grant "client_credentials")
-                    (send-json exchange 200 "{\"access_token\":\"app-token\",\"token_type\":\"bearer\",\"expires_in\":3600}")
-                    (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\",\"expires_in\":3600}"))))
+                  ;; Reddit token (client_credentials for import + authorization_code for login)
+                  (and (= method "POST") (str/ends-with? path "/api/v1/access_token"))
+                  (let [form (read-form exchange)
+                        grant (or (:grant form) "")
+                        code (or (:code form) "mock:t2_test:redditor")]
+                    (if (= grant "client_credentials")
+                      (send-json exchange 200 "{\"access_token\":\"app-token\",\"token_type\":\"bearer\",\"expires_in\":3600}")
+                      (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\",\"expires_in\":3600}"))))
 
-                ;; GitHub user
-                (= path "/user")
-                (let [token (bearer-token exchange)
-                      user (or (parse-token-user token) {:id "1002" :login "newbie" :numeric? true})]
-                  (send-json exchange 200
-                             (str "{\"id\":" (:id user) ",\"login\":\"" (:login user) "\"}")))
+                  ;; GitHub user
+                  (= path "/user")
+                  (let [token (bearer-token exchange)
+                        user (or (parse-token-user token) {:id "1002" :login "newbie" :numeric? true})]
+                    (send-json exchange 200
+                               (str "{\"id\":" (:id user) ",\"login\":\"" (:login user) "\"}")))
 
-                ;; Reddit /api/v1/me
-                (str/ends-with? path "/api/v1/me")
-                (let [token (bearer-token exchange)
-                      user (or (parse-token-user token) {:id "t2_test" :login "redditor"})]
-                  (send-json exchange 200
-                             (str "{\"id\":\"" (:id user) "\",\"name\":\"" (:login user) "\"}")))
+                  ;; Reddit /api/v1/me
+                  (str/ends-with? path "/api/v1/me")
+                  (let [token (bearer-token exchange)
+                        user (or (parse-token-user token) {:id "t2_test" :login "redditor"})]
+                    (send-json exchange 200
+                               (str "{\"id\":\"" (:id user) "\",\"name\":\"" (:login user) "\"}")))
 
-                :else
-                (send-json exchange 404 "{\"error\":\"not found\"}")))))]
+                  :else
+                  (send-json exchange 404 "{\"error\":\"not found\"}")))
+              (catch Throwable t
+                (binding [*out* *err*]
+                  (println "mock-oauth handler error:" t))
+                (try
+                  (send-json exchange 500 "{\"error\":\"mock-oauth internal\"}")
+                  (catch Throwable _))))))]
     (.createContext server "/" handler)
     (.setExecutor server nil)
     (.start server)
diff --git a/test/support/mock_reddit.clj b/test/support/mock_reddit.clj
index a630cf0938722193e9af88382d60e777ff371be4..faa27945b6394363914c853627a0bad1e819a5f9 100644
--- a/test/support/mock_reddit.clj
+++ b/test/support/mock_reddit.clj
@@ -12,7 +12,7 @@
 (defn- query-param [query key]
   (when query
     (some (fn [pair]
-            (let [[k v] (str/split pair "=" 2)]
+            (let [[k v] (str/split pair #"=" 2)]
               (when (= k key)
                 (URLDecoder/decode (or v "") "UTF-8"))))
           (str/split query #"&"))))
@@ -34,11 +34,11 @@
 
 (defn- send-redirect [^HttpExchange ex location]
   (.set (.getResponseHeaders ex) "Location" location)
-  (.sendResponseHeaders ex 302 -1)
+  (.sendResponseHeaders ex 302 0)
   (.close (.getResponseBody ex)))
 
 (defn- read-form [^HttpExchange ex]
-  (let [body (slurp (.getInputStream ex))]
+  (let [body (slurp (.getRequestBody ex))]
     {:code (query-param body "code")
      :grant (query-param body "grant_type")}))
 
@@ -48,7 +48,7 @@
           (str/replace #"^[Bb]earer " "")))
 
 (defn- parse-token-user [token]
-  (when (str/starts-with? token "mock:")
+  (when (and token (str/starts-with? token "mock:"))
     (parse-mock-user (subs token 5))))
 
 (defn start-mock-reddit
@@ -72,7 +72,7 @@
                        user (parse-mock-user (query-param query "mock_user"))
                        code (str "mock:" (:id user) ":" (:login user))
                        loc (str redirect-uri "?code=" (java.net.URLEncoder/encode code "UTF-8")
-                                "&state=" (java.net.URLEncoder/encode state "UTF-8"))]
+                                "&state=" (java.net.URLEncoder/encode (or state "") "UTF-8"))]
                    (send-redirect exchange loc))
 
                  (and (= method "POST") (str/ends-with? path "/api/v1/access_token"))

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.