Side A fixes real bugs in test infrastructure (regex vs string split, wrong HTTP methods on request body reading, incorrect sendResponseHeaders length, missing nil-guards causing crashes) that were actively breaking E2E auth flows, directly restoring test reliability. Side B adds a display feature exposing existing connectivity stats with new formatting logic and tests, which is useful but lower-stakes than fixing broken CI/test infra that blocks verification of core functionality.
constitution · epochs · watch · epoch 3
c_597d3f736194 (tommy-mor) vs c_d6d339485601 (tommy-mor)
download prompt · raw event · cmp_5ffdb8902f69c3
council reasoning
A fixes real crashes in the OAuth/Reddit mock HttpServer handlers (wrong body stream, string split vs regex, null token/state, response length) and restores working Playwright E2E auth, which is lasting test infrastructure. B only formats already-available ConnectivityStats for CLI display plus unit tests on that string, a useful but thinner presentation change.
Side A repairs broken end-to-end OAuth test infrastructure by fixing query parsing (`str/split` with regex), reading POST bodies correctly (`getRequestBody`), handling null tokens/states safely, correcting redirect responses, and wrapping mock handlers to avoid crashes, along with improving Playwright DOM selection and synchronization. Side B adds useful CLI visibility by formatting and displaying existing connectivity statistics with tests, but it is primarily a presentation enhancement rather than a correctness fix restoring critical test functionality.
sides
A — 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"))
B — c_d6d339485601 (tommy-mor)
message
[14749a34] Show graph topology with pair suggestions Expose existing connectivity statistics in CLI output so voters can see sparse or disconnected scopes before adding an edge. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 70435b412188a151c5e89e842a5de57f7480ddf2..abb5a55b49f60fe28fbfd4ec02715cb94ea0b4ec 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -397,6 +397,29 @@ fn print_item_response(resp: &ItemResponse) {
}
}
+fn format_connectivity_stats(stats: &ConnectivityStats) -> String {
+ let density = if stats.pairs_possible == 0 {
+ 0.0
+ } else {
+ stats.pairs_voted as f64 / stats.pairs_possible as f64 * 100.0
+ };
+ let component_label = if stats.components == 1 { "component" } else { "components" };
+ let connection_status = if stats.comparisons_until_connected == 0 {
+ "connected".to_string()
+ } else {
+ let comparison_label = if stats.comparisons_until_connected == 1 {
+ "comparison"
+ } else {
+ "comparisons"
+ };
+ format!("{} {comparison_label} to connect", stats.comparisons_until_connected)
+ };
+ format!(
+ "graph: {} items, {}/{} pairs ({density:.1}% density), {} {component_label}, {connection_status}",
+ stats.items, stats.pairs_voted, stats.pairs_possible, stats.components,
+ )
+}
+
fn print_pair_response(resp: &PairResponse) {
println!("{} vs {}", resp.left, resp.right);
if let Some(b) = &resp.left_body {
@@ -413,6 +436,10 @@ fn print_pair_response(resp: &PairResponse) {
println!();
println!("threads: {}", resp.threads.iter().map(|t| format!("#{t}")).collect::<Vec<_>>().join(" "));
}
+ if let Some(stats) = &resp.connectivity {
+ println!();
+ println!("{}", format_connectivity_stats(stats));
+ }
println!();
println!("---");
println!("Before writing your comparison, talk to your human. Ask them about their relationship to these two items — which one has meant more to them, whether their perspective has changed over time, and where they land now. Listen to what they say. Then write a comparison that synthesizes their perspective with your own analysis. The best submissions on slug.social carry both voices — your reasoning and their experience.");
@@ -816,6 +843,43 @@ fn write_secret_file(name: &str, contents: &str) -> Result<()> {
Ok(())
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn connectivity_stats_show_sparse_disconnected_graph() {
+ let stats = ConnectivityStats {
+ items: 9,
+ components: 3,
+ comparisons_until_connected: 2,
+ pairs_voted: 8,
+ pairs_possible: 36,
+ };
+
+ assert_eq!(
+ format_connectivity_stats(&stats),
+ "graph: 9 items, 8/36 pairs (22.2% density), 3 components, 2 comparisons to connect"
+ );
+ }
+
+ #[test]
+ fn connectivity_stats_show_connected_graph() {
+ let stats = ConnectivityStats {
+ items: 4,
+ components: 1,
+ comparisons_until_connected: 0,
+ pairs_voted: 3,
+ pairs_possible: 6,
+ };
+
+ assert_eq!(
+ format_connectivity_stats(&stats),
+ "graph: 4 items, 3/6 pairs (50.0% density), 1 component, connected"
+ );
+ }
+}
+
async fn run_scoped(base: &str, room: &str, sub: ScopedCmd) -> Result<()> {
let room = room.trim();
let client = http_client()?;
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.