constitution · epochs · watch · epoch 3

comparison

c_45dfa34511fe (tommy-mor) vs c_cd965c070df3 (tommy-mor)

download prompt · raw event · cmp_e166245f0ef79e

council reasoning

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

Side A consolidates duplicated room-route parsing logic into shared, validated helpers (room_route_segment/room_id_from_route_segment) used consistently across many endpoints and tests, and adds a substantive, well-tested URL-identity normalization layer (query sorting, YouTube canonicalization) that fixes real duplicate-identity bugs in the ontology system. Side B is a valid, narrower bugfix (Reddit child import wiring and unranked-label rendering) but touches far less surface area and adds less lasting infrastructure than A's DRY refactor plus new normalization module and its 20+ regression tests.

~x-ai/grok-latest · winner A · 3:1 · permalink

A adds lasting shared design: fixed-width room path encoding (`room_route_segment` / `room_id_from_route_segment`), route/nav/test alignment to `/r/{short}{slug}`, and external URL identity normalization (YouTube rewrites, sorted query pairs) used by `canonicalize_item`. B is a real but localized fix—Reddit children attach via `apply_entity_under_parent` without `ensure_path` nesting, plus title-based unranked labels—valuable for that import/UI path yet narrower lasting surface than A’s core routing and identity model.

openai/gpt-chat-latest · winner A · 4:1 · permalink

Side A introduces a durable routing abstraction (`room_route_segment`/`room_id_from_route_segment` and `ROOM_SHORT_ID_LEN`), updates all server routes and URL generation to use a single encoded room path format, and adds URL normalization utilities (including canonicalization of external URLs such as YouTube links) with accompanying tests. Side B fixes a real Reddit import bug by attaching imported children directly under the fetched parent and improves unranked child labels using stored titles, but its impact is narrower and localized compared with Side A's project-wide API and URL consistency improvements.

sides

A — c_45dfa34511fe (tommy-mor)

message

[5ca518f6] url refactor

diff preview

diff --git a/Cargo.lock b/Cargo.lock
index 67a09a3b54f778fa7e857fdd589c3ed9c92e1322..ad7e4fe6d4ba2f2b033916194c1ef1ed873f1d46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1757,6 +1757,7 @@ name = "slug-types"
 version = "0.1.0"
 dependencies = [
  "serde",
+ "url",
 ]
 
 [[package]]
@@ -2272,6 +2273,7 @@ dependencies = [
  "idna",
  "percent-encoding",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 606f7d6f97a4428efb90d1e0d544934c861fc4c5..cd3e0f0afd972d9ad9e7e4b92c5fa4c22bb8f620 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -270,10 +270,10 @@ fn post_redirect_location(room: &str, thread_tag: &str) -> String {
         format!("/t/{tag}")
     } else {
         let room = room.trim();
-        let Some((a, b)) = room.split_once('/') else {
+        let Some(seg) = slug_types::room_route_segment(room) else {
             return "/".to_string();
         };
-        format!("/r/{a}/{b}/t/{tag}")
+        format!("/r/{seg}/t/{tag}")
     }
 }
 
diff --git a/server/src/api/write_actor.rs b/server/src/api/write_actor.rs
index cb78d2f3f95b1bc163c1fb064d1d5f657e18000f..f9c3b8bd3fbf8fcb9c035e1a1572fef0b08fa8a9 100644
--- a/server/src/api/write_actor.rs
+++ b/server/src/api/write_actor.rs
@@ -19,13 +19,15 @@ use crate::{
 use super::auth::{issue_token_for_user, verify_token};
 use super::helpers::{now_ms, resolve_item};
 use super::validate::{normalize_room_and_thread, validate_ingest_document};
-use slug_types::RpcResult;
+use slug_types::{room_route_segment, RpcResult, ROOM_SHORT_ID_LEN};
 
 fn gen_short_id() -> String {
     use rand::Rng;
     const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
     let mut rng = rand::thread_rng();
-    (0..7).map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char).collect()
+    (0..ROOM_SHORT_ID_LEN)
+        .map(|_| ALPHABET[rng.gen_range(0..ALPHABET.len())] as char)
+        .collect()
 }
 
 fn parse_capability(s: &str) -> Result<crate::events::ThreadCapability, String> {
@@ -55,8 +57,8 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
     let feed_id = if room_key == "public" { "thread-feed" } else { "room-thread-feed" };
     let thread_url = if room_key == "public" {
         format!("/t/{thread_id}")
-    } else if let Some((short, slug)) = room_key.split_once('/') {
-        format!("/r/{short}/{slug}/t/{thread_id}")
+    } else if let Some(seg) = room_route_segment(room_key) {
+        format!("/r/{seg}/t/{thread_id}")
     } else {
         format!("/t/{thread_id}")
     };
@@ -78,8 +80,8 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
     let js = builder.build();
     let mut path_prefixes = vec![if room_key == "public" {
         "/".to_string()
-    } else if let Some((short, slug)) = room_key.split_once('/') {
-        format!("/r/{short}/{slug}")
+    } else if let Some(seg) = room_route_segment(room_key) {
+        format!("/r/{seg}")
     } else {
         "/".to_string()
     }];
diff --git a/server/src/html/forum/nav.rs b/server/src/html/forum/nav.rs
index 0ee33d91160fc5542817b5e3e9ab4fee1d0e600f..48fe11e46731670874ff8b6b05baa6f09ae0b7e4 100644
--- a/server/src/html/forum/nav.rs
+++ b/server/src/html/forum/nav.rs
@@ -1,7 +1,8 @@
 use crate::canonical_path::canonicalize_item;
 use crate::reducer::ScopeId;
+use slug_types::room_route_segment;
 
-/// URL helpers for public `/t/…` and private room threads `/r/{short}/{slug}/t/…`.
+/// URL helpers for public `/t/…` and private room threads `/r/{short}{slug}/t/…`.
 #[derive(Clone)]
 pub struct ThreadNav {
     pub room_wire: String,
@@ -22,18 +23,15 @@ impl ThreadNav {
         }
     }
 
-    /// `room_id` wire form `shortid/slug`.
+    /// `room_id` wire form `shortid/slug` (HTTP uses [`slug_types::room_route_segment`]).
     pub(crate) fn from_room_id(room_id: &str) -> Option<Self> {
-        let (short, slug) = room_id.split_once('/')?;
-        if short.is_empty() || slug.is_empty() {
-            return None;
-        }
+        let room_seg = room_route_segment(room_id)?;
         Some(Self {
             room_wire: room_id.to_string(),
             scope: ScopeId::Room(room_id.to_string()),
-            room_path: format!("/r/{short}/{slug}"),
-            thread_path_prefix: format!("/r/{short}/{slug}/t"),
-            garden_path_prefix: format!("/r/{short}/{slug}/~"),
+            room_path: format!("/r/{room_seg}"),
+            thread_path_prefix: format!("/r/{room_seg}/t"),
+            garden_path_prefix: format!("/r/{room_seg}/~"),
         })
     }
 
diff --git a/server/src/html/forum/post_single.rs b/server/src/html/forum/post_single.rs
index c316f8f836df9d4ef9c05ebd9e54f699540e6d72..473747b3da3d4d7a54b5e0c63165d2533df643e1 100644
--- a/server/src/html/forum/post_single.rs
+++ b/server/src/html/forum/post_single.rs
@@ -93,12 +93,14 @@ pub async fn thread_post_view(
 
 pub async fn room_thread_post_view(
     State(state): State<AppState>,
-    Path((room_short, room_slug, tag, index_str)): Path<(String, String, String, String)>,
+    Path((room_key, tag, index_str)): Path<(String, String, String)>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
diff --git a/server/src/html/forum/views.rs b/server/src/html/forum/views.rs
index be5df1745ef580891a167c23c3dd6c06f804f295..1ec421f84335cbfe7f9db775b73a8ed24b197174 100644
--- a/server/src/html/forum/views.rs
+++ b/server/src/html/forum/views.rs
@@ -183,16 +183,18 @@ pub async fn thread_view(
     thread_view_inner(state, tag, q, ThreadNav::public(), headers, jar, uri).await
 }
 
-/// Room thread — `/r/:short/:slug/t/:tag`
+/// Room thread — `/r/:room_key/t/:tag` (`room_key` = `{short}{slug}`).
 pub async fn room_thread_view(
     State(state): State<AppState>,
-    Path((room_short, room_slug, tag)): Path<(String, String, String)>,
+    Path((room_key, tag)): Path<(String, String)>,
     Query(q): Query<ThreadViewQuery>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
@@ -226,15 +228,17 @@ pub(super) fn room_not_found_page(jar: &CookieJar, uri: &Uri) -> impl IntoRespon
     (StatusCode::NOT_FOUND, Html(page.into_string()))
 }
 
-/// Private room index — `/r/:short/:slug`
+/// Private room index — `/r/:room_key`
 pub async fn room_page(
     State(state): State<AppState>,
-    Path((room_short, room_slug)): Path<(String, String)>,
+    Path(room_key): Path<String>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "room not found").into_response();
+    };
     let now = now_ms();
     let reduced = state.reduced.read().await;
     if !reduced.rooms.contains(&room_id) {
@@ -266,7 +270,10 @@ pub async fn room_page(
     let audit_cli = format!("npx slugsocial private {room_id} audit");
     drop(reduced);
 
-    let slug_display = room_slug.as_str();
+    let slug_display = room_id
+        .split_once('/')
+        .map(|(_, slug)| slug)
+        .unwrap_or(room_id.as_str());
     let page = layout(
         &format!("room {slug_display} — slug.social"),
         "view-thread",
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 423f23fd8c9ad7b7f454d6ea7a9a7607a4c9c5b9..e615dd356bcf634232d85610c0a26235ead125fd 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -309,12 +309,14 @@ pub async fn external_ontology_path(
 
 pub async fn room_garden_index(
     State(state): State<AppState>,
-    Path((room_short, room_slug)): Path<(String, String)>,
+    Path(room_key): Path<String>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
@@ -341,12 +343,14 @@ pub async fn room_garden_index(
 
 pub async fn room_external_garden_index(
     State(state): State<AppState>,
-    Path((room_short, room_slug)): Path<(String, String)>,
+    Path(room_key): Path<String>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
@@ -416,12 +420,14 @@ pub async fn room_external_garden_index(
 
 pub async fn room_external_ontology_path(
     State(state): State<AppState>,
-    Path((room_short, room_slug, path)): Path<(String, String, String)>,
+    Path((room_key, path)): Path<(String, String)>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
@@ -442,12 +448,14 @@ pub async fn room_external_ontology_path(
 
 pub async fn room_ontology_path(
     State(state): State<AppState>,
-    Path((room_short, room_slug, path)): Path<(String, String, String)>,
+    Path((room_key, path)): Path<(String, String)>,
     headers: HeaderMap,
     jar: CookieJar,
     uri: Uri,
 ) -> impl IntoResponse {
-    let room_id = format!("{room_short}/{room_slug}");
+    let Some(room_id) = slug_types::room_id_from_route_segment(&room_key) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let Some(nav) = ThreadNav::from_room_id(&room_id) else {
         return (StatusCode::NOT_FOUND, "bad room path").into_response();
     };
diff --git a/server/src/html/search.rs b/server/src/html/search.rs
index 43e6ebf36cf0fe72c96f0f9d850bea51ac094c43..f01732f7edb32c68fcc10c39545c8f56476adb27 100644
--- a/server/src/html/search.rs
+++ b/server/src/html/search.rs
@@ -351,8 +351,8 @@ fn render_search_results(results: &SearchResults, query: &str) -> Markup {
                     ul class="search-posts" {
                         @for r in &results.posts {
                             @let (post_href, post_label) = if let Some((room, tag)) = r.thread.split_once("/#") {
-                                if let Some((short, slug)) = room.split_once('/') {
-                                    (format!("/r/{short}/{slug}/t/{tag}"), format!("{room}/#{tag}"))
+                                if let Some(seg) = slug_types::room_route_segment(room) {
+                           

… preview truncated; 35,812 characters omitted

download full diff A

B — c_cd965c070df3 (tommy-mor)

message

[993d359c] Fix Reddit children import wiring and unranked child labels.

Listing imports attach posts directly under the subreddit without ensure_path
pulling comment-path segments in, and the ranking panel shows imported titles.
Update integration tests for JS SSE morphs and children fetch.

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

diff preview

diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index d1defd28242fd2ca3b886adc91a7070bef75e653..e649a7d192feade465e19ce6187a829f6ec74372 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -58,7 +58,7 @@ pub async fn post_ui_html(
             let tree = state.tree.read().await;
             let empty = crate::reducer::NodeState::default();
             let node = tree.get(&parent).unwrap_or(&empty);
-            let panel = ranking_panel(&parent, node);
+            let panel = ranking_panel(&parent, node, &tree);
             JsBuilder::new()
                 .morph_selector("#ranking-panel", panel)
                 .into_response()
diff --git a/server/src/fetch/mod.rs b/server/src/fetch/mod.rs
index 0177bb161cea1b72a100b52efdfc5e710271c9eb..35f968c21c69ca557bab7951413e3cfbbccfebfd 100644
--- a/server/src/fetch/mod.rs
+++ b/server/src/fetch/mod.rs
@@ -107,7 +107,7 @@ pub fn fetch_entity_stream(
                 let mut b = JsBuilder::new()
                     .morph_selector("#entity-section", html::entity_section(&id, node, false));
                 if kind == FetchKind::Children {
-                    b = b.morph_selector("#ranking-panel", ranking_panel(&id, node));
+                    b = b.morph_selector("#ranking-panel", ranking_panel(&id, node, &tree));
                 }
                 yield Ok(js_event(b.build()));
             }
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 27ce9118c73ec5643e04363ab1a36cf5da6101bf..e88cc43ddc9d8100f7994be6f5960ec4d8f22c55 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -15,7 +15,7 @@ use crate::{
     ranking::{
         connected_components_from_voted_pairs, ranked_items_subset, RankedItem, MAX_ITERS, TOL,
     },
-    reducer::NodeState,
+    reducer::{GlobalTree, NodeState},
     state::AppState,
     ui_action::UI_RPC_FIELD,
 };
@@ -182,8 +182,15 @@ fn display_label(id: &ItemId) -> String {
         .to_string()
 }
 
+fn child_label(tree: &GlobalTree, id: &ItemId) -> String {
+    tree.get(id)
+        .and_then(|n| n.data.as_ref())
+        .map(|d| d.title.clone())
+        .unwrap_or_else(|| display_label(id))
+}
+
 /// Plain (unscored) list of children that have no votes yet.
-fn unranked_list(label: &str, items: &[ItemId]) -> Markup {
+fn unranked_list(label: &str, items: &[ItemId], tree: &GlobalTree) -> Markup {
     html! {
         @if !items.is_empty() {
             h3 class="rank-heading muted small" { (label) }
@@ -191,7 +198,7 @@ fn unranked_list(label: &str, items: &[ItemId]) -> Markup {
                 @for it in items {
                     li {
                         a href=(item_href(it)) {
-                            strong { (display_label(it)) }
+                            strong { (child_label(tree, it)) }
                         }
                     }
                 }
@@ -200,7 +207,7 @@ fn unranked_list(label: &str, items: &[ItemId]) -> Markup {
     }
 }
 
-pub fn ranking_panel(item: &ItemId, node: &NodeState) -> Markup {
+pub fn ranking_panel(item: &ItemId, node: &NodeState, tree: &GlobalTree) -> Markup {
     let group = &node.local_ranking;
     let n = group.idx_to_item.len();
     let (comps, _isolates) =
@@ -248,7 +255,7 @@ pub fn ranking_panel(item: &ItemId, node: &NodeState) -> Markup {
                     @let label = if multi { format!("Ranking group {}", gi + 1) } else { "Ranking".to_string() };
                     (rank_list(&label, ranked, 1))
                 }
-                (unranked_list("Unranked", &unranked))
+                (unranked_list("Unranked", &unranked, tree))
             }
         }
     }
@@ -297,7 +304,7 @@ async fn item_page(state: AppState, uri: Uri, item: ItemId) -> Markup {
         (input_panel("", None))
         (breadcrumb_path(&item))
         (entity_section(&item, node, false))
-        (ranking_panel(&item, node))
+        (ranking_panel(&item, node, &tree))
     };
     layout("sorter2", body, views)
 }
diff --git a/server/src/reddit.rs b/server/src/reddit.rs
index a0eb688709478ee0185b953b41a5d26cc354764d..69d979bc7e4a1cb078f70114dc539bdc1986b574 100644
--- a/server/src/reddit.rs
+++ b/server/src/reddit.rs
@@ -300,9 +300,16 @@ async fn reddit_worker(
                     }
                     {
                         let mut tree = tree.write().await;
-                        apply_entity_import(&mut tree, &child_id, child_payload);
                         if kind == FetchKind::Children {
-                            tree.link_child(&fetch_id, &child_id);
+                            let view = entity_view_from_payload(&child_id, &child_payload);
+                            tree.apply_entity_under_parent(
+                                &fetch_id,
+                                &child_id,
+                                child_payload,
+                                view,
+                            );
+                        } else {
+                            apply_entity_import(&mut tree, &child_id, child_payload);
                         }
                     }
                     written += 1;
diff --git a/server/src/reducer.rs b/server/src/reducer.rs
index a36cd5c9287d61536f9f4a3f6b0df2342388857a..4e42d0369dab50bb2f8ca664aa69b628292f6c07 100644
--- a/server/src/reducer.rs
+++ b/server/src/reducer.rs
@@ -209,14 +209,24 @@ impl GlobalTree {
         }
     }
 
-    /// Directly attach `child` under `parent`, bypassing path-based nesting.
-    /// Used for imported listings (e.g. a subreddit's posts) so they show up
-    /// as children of the subreddit rather than a deep `…/comments/<id>` path.
-    pub fn link_child(&mut self, parent: &ItemId, child: &ItemId) {
+    /// Import entity data for `id` and attach it as a direct child of `parent`
+    /// without running [`Self::ensure_path`] on `id` (avoids Reddit `/comments/`
+    /// parent rules pulling intermediate path segments into the subreddit).
+    pub fn apply_entity_under_parent(
+        &mut self,
+        parent: &ItemId,
+        id: &ItemId,
+        payload: Value,
+        view: Option<EntityData>,
+    ) {
         self.ensure_path(parent);
-        self.ensure_path(child);
+        self.ensure_node(id);
+        if let Some(node) = self.nodes.get_mut(id) {
+            node.entity_raw = Some(payload);
+            node.data = view;
+        }
         if let Some(p) = self.nodes.get_mut(parent) {
-            p.children.insert(child.clone());
+            p.children.insert(id.clone());
         }
     }
 }
diff --git a/test/reddit_import.clj b/test/reddit_import.clj
index 84cbdcf7965e50290313cbce2243a16b583d2097..45a2a19f20799d77e84d8aa64735ab5e7e45f97c 100644
--- a/test/reddit_import.clj
+++ b/test/reddit_import.clj
@@ -67,6 +67,36 @@
           (do (Thread/sleep 200) (recur))
           false)))))
 
+(defn- run-reddit-fetch-assertions [app-base data-dir]
+  (let [browse-url (str app-base "/~/https://reddit.com/r/rust")
+        log-path (str data-dir "/events.jsonl")
+        before (:out (process/shell {:out :string :err :string}
+                                    "curl" "-sf" browse-url))]
+    (is (str/includes? before "Fetch from Reddit"))
+    (is (not (str/includes? before "The Rust Programming Language")))
+    (let [sse (curl-fetch-ui-sse app-base "reddit.com/r/rust" "self")]
+      (is (zero? (:exit sse)) "POST /ui fetch_entity (self) SSE succeeds")
+      (is (str/includes? (:out sse) "Idiomorph.morph"))
+      (is (str/includes? (:out sse) "The Rust Programming Language"))
+      (is (wait-event-log log-path 2000) "event log written"))
+    (let [after (:out (process/shell {:out :string :err :string}
+                                     "curl" "-sf" browse-url))
+          log (slurp (io/file log-path))]
+      (is (str/includes? after "The Rust Programming Language"))
+      (is (str/includes? log "\"type\":\"entity_imported\""))
+      (is (str/includes? log "\"subscribers\":350000"))
+      (is (str/includes? log "\"display_name\":\"rust\"")))
+    (let [children-sse (curl-fetch-ui-sse app-base "reddit.com/r/rust" "children")]
+      (is (zero? (:exit children-sse)) "POST /ui fetch_entity (children) SSE succeeds")
+      (is (str/includes? (:out children-sse) "Idiomorph.morph"))
+      (is (str/includes? (:out children-sse) "Announcing Rust 1.99")))
+    (let [after-children (:out (process/shell {:out :string :err :string}
+                                              "curl" "-sf" browse-url))
+          log2 (slurp (io/file log-path))]
+      (is (str/includes? after-children "Announcing Rust 1.99"))
+      (is (str/includes? after-children "Unranked"))
+      (is (str/includes? log2 "announcing_rust_199")))))
+
 (deftest reddit-fetch-via-mock-api
   (testing "Fetch more queues import; event log stores full payload; page shows title"
     (let [root (repo-root)
@@ -102,24 +132,7 @@
                                     bin)]
           (try
             (is (wait-health app-base 20000) "app healthz")
-            (let [browse-url (str app-base "/~/https://reddit.com/r/rust")
-                  before (:out (process/shell {:out :string :err :string}
-                                              "curl" "-sf" browse-url))]
-              (is (str/includes? before "Fetch from Reddit"))
-              (is (not (str/includes? before "The Rust Programming Language")))
-              (let [log-path (str data-dir "/events.jsonl")
-                    sse (curl-fetch-ui-sse app-base "reddit.com/r/rust")]
-                (is (zero? (:exit sse)) "POST /ui fetch_entity SSE succeeds")
-                (is (str/includes? (:out sse) "event: complete"))
-                (is (str/includes? (:out sse) "The Rust Programming Language"))
-                (is (wait-event-log log-path 2000) "event log written")
-                (let [after (:out (process/shell {:out :string :err :string}
-                                                 "curl" "-sf" browse-url))
-                      log (slurp (io/file log-path))]
-                  (is (str/includes? after "The Rust Programming Language"))
-                  (is (str/includes? log "\"type\":\"entity_imported\""))
-                  (is (str/includes? log "\"subscribers\":350000"))
-                  (is (str/includes? log "\"display_name\":\"rust\"")))))
+            (run-reddit-fetch-assertions app-base data-dir)
             (finally
               (process/destroy proc))))
         (finally
diff --git a/test/smoke.clj b/test/smoke.clj
index 11887c48282088e140d823a88ba616f6325835b3..ce9f958c84b9a89ae55e215ab519f1df6435e24b 100644
--- a/test/smoke.clj
+++ b/test/smoke.clj
@@ -49,7 +49,7 @@
           (is (wait-health base 15000) "server responds to /healthz")
           (let [home (:out (process/shell {:out :string :err :string}
                                          "curl" "-sf" (str base "/")))]
-            (is (str/includes? home "vote-panel"))
+            (is (str/includes? home "entity-section"))
             (is (str/includes? home "ranking-panel"))
             (is (str/includes? home "parser-panel"))
             (is (str/includes? home "__rpc__")))

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.