Side B fixes a real functional bug (Reddit children imports bypassing ensure_path and showing raw IDs instead of titles), threading tree context through several call sites and updating integration tests to verify the fix. Side A tightens validation constraints (0:0 handling, max 100) which is useful but more incremental/policy-level, with much of its diff being test churn and message-text changes rather than new behavior.
constitution · epochs · watch · epoch 3
c_55666fe32c48 (tommy-mor) vs c_cd965c070df3 (tommy-mor)
download prompt · raw event · cmp_4728ebb3958e4e
council reasoning
B fixes real Reddit import behavior by replacing incomplete link_child/ensure_path wiring with apply_entity_under_parent so listing children attach under the subreddit without comment-path segments, and makes unranked labels use entity titles via child_label/ranking_panel(tree). A only tightens vote-ratio validation (reject 0-sides and >100) across parser/UI/reducer with regression tests—useful invariants, but less product-critical than the import and display fix.
Side A closes a correctness gap by enforcing vote ratio constraints consistently across the DSL parser, browser POST handler, and reducer, preventing invalid zero-sided votes and oversized ratios while adding targeted unit and integration regression tests. Side B fixes Reddit child import attachment and improves unranked labels by importing children under the intended parent and passing tree context into the ranking panel, but its impact is narrower to the Reddit import/UI path compared with A's project-wide data validation and invariant enforcement.
sides
A — c_55666fe32c48 (tommy-mor)
message
[aa0b175c] Enforce vote ratio constraints: both sides ≥ 1, max 100. Zero on either side produces no valid graph edge; ratios above 100 add no meaningful signal. Enforce in the DSL parser, browser POST handler, and reducer guard. Update browser pool test to use 99:1 instead of 100:0. Add unit and integration regression tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff preview
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index b79efdb4d52bd445a67f38cbfd61d3507d2b3014..bc8a0130b434cc7880a4bf16eb9237080c4aa383 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -243,11 +243,23 @@ async fn dispatch_ui_action(
let pool_id = pool.as_deref().and_then(|p| {
crate::path_types::ItemId::parse(p.trim()).map(|i| i.normalized_storage())
});
- let mut rl = ratio_left.trim().parse::<i32>().unwrap_or(0).max(0);
- let mut rr = ratio_right.trim().parse::<i32>().unwrap_or(0).max(0);
- if rl == 0 && rr == 0 {
- rl = 1;
- rr = 1;
+ let rl = ratio_left.trim().parse::<i32>().unwrap_or(0).max(0);
+ let rr = ratio_right.trim().parse::<i32>().unwrap_or(0).max(0);
+ if rl == 0 || rr == 0 {
+ return form_js_error(
+ err_tgt.as_ref(),
+ "invalid ratio",
+ "Both ratio sides must be ≥ 1.",
+ )
+ .into_response();
+ }
+ if rl > 100 || rr > 100 {
+ return form_js_error(
+ err_tgt.as_ref(),
+ "invalid ratio",
+ "Ratio sides must be ≤ 100.",
+ )
+ .into_response();
}
let text = format!(
diff --git a/server/src/dsl.rs b/server/src/dsl.rs
index faa8aac6616bc6ea2b102d08ae999c01a716ef6e..338feacc579a92bed291867cc0f2f62462737462 100644
--- a/server/src/dsl.rs
+++ b/server/src/dsl.rs
@@ -545,9 +545,14 @@ fn parse_block_prefixed_statement(
let ((ratio_left, ratio_right), k) = parse_comparison_at(s, i)
.ok_or_else(|| DslError::Parse(format!("invalid comparison near: {}", &s[i..])))?;
- if ratio_left == 0 && ratio_right == 0 {
+ if ratio_left == 0 || ratio_right == 0 {
return Err(DslError::Parse(
- "vote ratio 0:0 is invalid; use 1:1 for a tie or omit the vote".to_string(),
+ "vote ratio sides must be ≥ 1; use 1:1 for a tie or omit the vote".to_string(),
+ ));
+ }
+ if ratio_left > 100 || ratio_right > 100 {
+ return Err(DslError::Parse(
+ "vote ratio sides must be ≤ 100".to_string(),
));
}
i = skip_ws(s, k);
@@ -929,11 +934,50 @@ mod tests {
let err = parse_full("{tie placeholder}\n~/a 0:0 ~/b").unwrap_err();
let DslError::Parse(msg) = err;
assert!(
- msg.contains("0:0"),
- "expected 0:0 rejection message, got: {msg}"
+ msg.contains("≥ 1"),
+ "expected zero-side rejection message, got: {msg}"
);
}
+ #[test]
+ fn parse_vote_rejects_left_zero_ratio() {
+ let err = parse_full("{prefer b}\n~/a 0:5 ~/b").unwrap_err();
+ let DslError::Parse(msg) = err;
+ assert!(
+ msg.contains("≥ 1"),
+ "expected zero-side rejection message, got: {msg}"
+ );
+ }
+
+ #[test]
+ fn parse_vote_rejects_right_zero_ratio() {
+ let err = parse_full("{prefer a}\n~/a 5:0 ~/b").unwrap_err();
+ let DslError::Parse(msg) = err;
+ assert!(
+ msg.contains("≥ 1"),
+ "expected zero-side rejection message, got: {msg}"
+ );
+ }
+
+ #[test]
+ fn parse_vote_rejects_over_max_ratio() {
+ let err = parse_full("{prefer a strongly}\n~/a 101:1 ~/b").unwrap_err();
+ let DslError::Parse(msg) = err;
+ assert!(
+ msg.contains("≤ 100"),
+ "expected max ratio rejection message, got: {msg}"
+ );
+ }
+
+ #[test]
+ fn parse_vote_accepts_max_ratio() {
+ let doc = parse_full("{prefer a}\n~/a 100:1 ~/b").unwrap();
+ assert!(matches!(
+ doc.statements.last(),
+ Some(Stmt::Vote { ratio_left: 100, ratio_right: 1, .. })
+ ));
+ }
+
#[test]
fn parse_full_interleaves_prose() {
let input = "hello\n#tag\nworld";
diff --git a/server/src/reducer.rs b/server/src/reducer.rs
index e949e8e092297eedd5c20c131f16ef0121f329b3..6841d35cfc9de2389f340a22b8a45acb335e36c3 100644
--- a/server/src/reducer.rs
+++ b/server/src/reducer.rs
@@ -119,11 +119,11 @@ impl GroupState {
let (i, j) = if a_idx < b_idx { (a_idx, b_idx) } else { (b_idx, a_idx) };
self.voted_pairs.insert((i, j));
- let mut w_a = vote.ratio_left.max(0) as f64;
- let mut w_b = vote.ratio_right.max(0) as f64;
- if w_a == 0.0 && w_b == 0.0 {
- w_a = 1.0;
- w_b = 1.0;
+ let w_a = vote.ratio_left as f64;
+ let w_b = vote.ratio_right as f64;
+ if w_a == 0.0 || w_b == 0.0 {
+ // Zero on either side produces no valid edge; drop the vote.
+ return;
}
self.add_edge_weight(b_idx, a_idx, w_a);
diff --git a/server/tests/basic.rs b/server/tests/basic.rs
index 9d83e7a97e2c7494790db17c4b5b30c181705026..cc8c1a0d139f3722ba6ecd13dd001c65be835b67 100644
--- a/server/tests/basic.rs
+++ b/server/tests/basic.rs
@@ -175,14 +175,14 @@ fn reducer_clamps_score_bounds() {
let mut state = ReducerState::default();
state.apply_event(ingest_event(
1,
- "@00000000-0000-0000-0000-000000000000:test:local/test\n~/t/a {a}\n~/t/b {b}\n{huge}\n~/t/a 1000:1 ~/t/b\n",
+ "@00000000-0000-0000-0000-000000000000:test:local/test\n~/t/a {a}\n~/t/b {b}\n{huge}\n~/t/a 100:1 ~/t/b\n",
));
state.apply_event(ingest_event(
2,
- "@00000000-0000-0000-0000-000000000000:test:local/test\n{huge}\n~/t/a 1:1000 ~/t/b\n",
+ "@00000000-0000-0000-0000-000000000000:test:local/test\n{huge}\n~/t/a 1:100 ~/t/b\n",
));
- assert_eq!(state.public().ranking_group.idx_to_item.len(), 2); // Should still work, scores clamped internally
+ assert_eq!(state.public().ranking_group.idx_to_item.len(), 2); // Should still work, scores handled internally
}
// ============================================================================
@@ -513,8 +513,8 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() {
.expect_err("0:0 vote must be rejected by the parser");
let slugsocial_server::dsl::DslError::Parse(msg) = err;
assert!(
- msg.contains("0:0"),
- "expected message about invalid 0:0 ratio, got: {msg}"
+ msg.contains("≥ 1"),
+ "expected message about invalid zero ratio, got: {msg}"
);
let mut state = ReducerState::default();
@@ -533,7 +533,7 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() {
#[test]
fn reducer_negative_ratio_clamped_to_zero() {
let _state = ReducerState::default();
- // GroupState::apply_vote clamps negatives to 0, then 0:0 -> 1:1
+ // GroupState::apply_vote clamps negatives to 0; when either side is 0 the vote is dropped.
let mut group = GroupState::new();
group.apply_vote(slugsocial_server::reducer::VoteData {
ts: 1,
@@ -546,12 +546,12 @@ fn reducer_negative_ratio_clamped_to_zero() {
delegate: Some("00000000-0000-0000-0000-000000000000:test:local/test".to_string()),
thread_tag: "t".to_string(),
});
+ // Items are registered, but the zero-clamped vote produces no edges.
assert_eq!(group.idx_to_item.len(), 2);
- // Both edges should exist (negatives clamped to 0, then 0:0 -> 1:1)
let a_idx = group.item_to_idx[&item_id("https://slug.social/~/t/a")];
let b_idx = group.item_to_idx[&item_id("https://slug.social/~/t/b")];
- assert!(group.edges.contains_key(&(a_idx, b_idx)));
- assert!(group.edges.contains_key(&(b_idx, a_idx)));
+ assert!(!group.edges.contains_key(&(a_idx, b_idx)));
+ assert!(!group.edges.contains_key(&(b_idx, a_idx)));
}
diff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs
index 23db7b5672418d5bb0ab7529e05ef1f89e59062a..09c5481f039d59efb617a82bafcc432a186e2d30 100644
--- a/server/tests/integration_ui.rs
+++ b/server/tests/integration_ui.rs
@@ -256,3 +256,96 @@ async fn test_sse_public_thread_morph_includes_post_body_not_thread_not_found()
);
}
+fn ui_vote_compare_post_rpc(
+ room: &str,
+ thread_tag: &str,
+ left: &str,
+ right: &str,
+ ratio_left: &str,
+ ratio_right: &str,
+ explanation: &str,
+) -> String {
+ serde_json::json!({
+ "action": "vote_compare_post",
+ "room": room,
+ "thread_tag": thread_tag,
+ "left_item": left,
+ "right_item": right,
+ "ratio_left": ratio_left,
+ "ratio_right": ratio_right,
+ "explanation": explanation,
+ "next": "/vote",
+ })
+ .to_string()
+}
+
+#[tokio::test]
+async fn test_vote_compare_post_rejects_zero_left_ratio() {
+ let (addr, _tmp, _log, _handle) = create_test_server().await;
+ let client = reqwest::Client::new();
+ let bearer = test_bearer();
+
+ let rpc = ui_vote_compare_post_rpc("public", "test-vote", "~/a", "~/b", "0", "5", "prefer b");
+ let resp = client
+ .post(format!("http://{addr}/ui"))
+ .header("Authorization", format!("Bearer {bearer}"))
+ .form(&[("__rpc__", rpc.as_str())])
+ .send()
+ .await
+ .unwrap();
+
+ assert_eq!(resp.status(), reqwest::StatusCode::OK);
+ let js = resp.text().await.unwrap();
+ assert!(
+ js.contains("invalid ratio") || js.contains("≥ 1"),
+ "expected zero-ratio rejection, got: {js}"
+ );
+}
+
+#[tokio::test]
+async fn test_vote_compare_post_rejects_zero_right_ratio() {
+ let (addr, _tmp, _log, _handle) = create_test_server().await;
+ let client = reqwest::Client::new();
+ let bearer = test_bearer();
+
+ let rpc = ui_vote_compare_post_rpc("public", "test-vote", "~/a", "~/b", "5", "0", "prefer a");
+ let resp = client
+ .post(format!("http://{addr}/ui"))
+ .header("Authorization", format!("Bearer {bearer}"))
+ .form(&[("__rpc__", rpc.as_str())])
+ .send()
+ .await
+ .unwrap();
+
+ assert_eq!(resp.status(), reqwest::StatusCode::OK);
+ let js = resp.text().await.unwrap();
+ assert!(
+ js.contains("invalid ratio") || js.contains("≥ 1"),
+ "expected zero-ratio rejection, got: {js}"
+ );
+}
+
+#[tokio::test]
+async fn test_vote_compare_post_rejects_over_max_ratio() {
+ let (addr, _tmp, _log, _handle) = create_test_server().await;
+ let client = reqwest::Client::new();
+ let bearer = test_bearer();
+
+ let rpc =
+ ui_vote_compare_post_rpc("public", "test-vote", "~/a", "~/b", "101", "1", "prefer a");
+ let resp = client
+ .post(format!("http://{addr}/ui"))
+ .header("Authorization", format!("Bearer {bearer}"))
+ .form(&[("__rpc__", rpc.as_str())])
+ .send()
+ .await
+ .unwrap();
+
+ assert_eq!(resp.status(), reqwest::StatusCode::OK);
+ let js = resp.text().await.unwrap();
+ assert!(
+ js.contains("invalid ratio") || js.contains("≤ 100"),
+ "expected over-max ratio rejection, got: {js}"
+ );
+}
+
diff --git a/test/browser_vote_pool.clj b/test/browser_vote_pool.clj
index 23d0bd80b02bd8b1b48853454bed02793296550e..738608801027d1e662660b5d6c830b4ccd07c434 100644
--- a/test/browser_vote_pool.clj
+++ b/test/browser_vote_pool.clj
@@ -38,8 +38,8 @@
;; 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
+ [99 1] ; left is earlier → prefer left
+ [1 99])] ; 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 "'"))))
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__")))
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.