Side B fixes a real off-by-one indexing bug (thread_post_index was 1-indexed but used as a 0-indexed URL path, causing broken links and an always-true 'index > 0' guard hiding index 0), adds test coverage asserting the corrected values, and updates documentation accordingly. Side A is purely cosmetic clippy cleanup (formatting, lint suppressions, minor idiom changes) with no behavioral impact, aside from trivially enabling one previously-unregistered test.
constitution · epochs · watch · epoch 3
c_88200cfa5c4d (tommy-mor) vs c_6a02ffb06a41 (tommy-mor)
download prompt · raw event · cmp_31612613ad084c
council reasoning
B fixes real index semantics: thread_post_index becomes required 0-based (aligned with /t/tag/N), drops the silent unwrap_or(0)/optional UI branch, and adds integration assertions plus doc clarity. A is almost entirely clippy/style churn (destructure matches, is_some_and, allow(dead_code), unused binds) plus enabling one already-written test—useful hygiene but little lasting design or bugfix value.
Side B makes a substantive behavioral fix by changing rank history to use a 0-based thread_post_index that matches `/t/tag/N` routes, replacing a silent `unwrap_or(0)` fallback with an `expect(...)` to enforce the data invariant, updating rendering, documentation, and integration tests accordingly. Side A is almost entirely Clippy-driven cleanup and minor test maintenance, with its only functional change being adding a missing `#[test]` attribute; most other edits are stylistic or lint-related.
sides
A — c_88200cfa5c4d (tommy-mor)
message
[10c9caac] Fix all workspace clippy warnings. Wire up a missing ui_action test, allow dead code in shared integration helpers, and apply small clippy cleanups across server and types. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/server/src/dsl.rs b/server/src/dsl.rs
index a45c5b33e10d0d2ea48c7313061cfa1e6430bcfc..faa8aac6616bc6ea2b102d08ae999c01a716ef6e 100644
--- a/server/src/dsl.rs
+++ b/server/src/dsl.rs
@@ -927,9 +927,7 @@ mod tests {
#[test]
fn parse_vote_rejects_zero_zero_ratio() {
let err = parse_full("{tie placeholder}\n~/a 0:0 ~/b").unwrap_err();
- let msg = match err {
- DslError::Parse(m) => m,
- };
+ let DslError::Parse(msg) = err;
assert!(
msg.contains("0:0"),
"expected 0:0 rejection message, got: {msg}"
diff --git a/server/src/html/garden/tests.rs b/server/src/html/garden/tests.rs
index c2036fca7752aef63d260e36cfe9649f63b5c550..0a1be1290fdc93197ca191197a473840bb4decbc 100644
--- a/server/src/html/garden/tests.rs
+++ b/server/src/html/garden/tests.rs
@@ -346,7 +346,7 @@ fn vote_compare_item_card_renders_github_import_markup() {
"headline": "#1 Compare card",
"sublines": ["State: open"],
});
- let body = format!("```slug-github-card\n{}\n```", json.to_string());
+ let body = format!("```slug-github-card\n{json}\n```");
let html = vote_compare_item_card(
&nav,
&item,
diff --git a/server/src/html/ui_action.rs b/server/src/html/ui_action.rs
index 2589a2cc00bb7b18cd19ebcbb938083122d6921d..5e4131dd346a6f80bfe091a9b0cf7902721bc47f 100644
--- a/server/src/html/ui_action.rs
+++ b/server/src/html/ui_action.rs
@@ -246,6 +246,7 @@ mod tests {
);
}
+ #[test]
fn set_new_thread_compose_expanded_true() {
let template = serde_json::json!({
"action": "set_new_thread_compose_expanded",
diff --git a/server/src/offline.rs b/server/src/offline.rs
index 2db6f67e22e00a24ce673d13095644dd9fa9342d..93d4d5ee55449da644f732bc0ba007ee4c2c7079 100644
--- a/server/src/offline.rs
+++ b/server/src/offline.rs
@@ -167,7 +167,7 @@ fn rankings_for_simulated(
.iter()
.map(|parent| {
let scoped_content = simulated
- .content_for_scope(&scope)
+ .content_for_scope(scope)
.unwrap_or_else(|| simulated.public());
let scoped = build_children_rankings(scoped_content, parent);
let components: Vec<RankComponent> = scoped
@@ -254,7 +254,9 @@ fn ingest_parse_error(raw: &str) -> Option<String> {
dsl::parse_full(raw).err().map(|e| e.to_string())
}
-fn load_events_from_jsonl(path: &Path) -> Result<(usize, Vec<(usize, Event)>, Vec<BadJsonLine>), std::io::Error> {
+type JsonlEventsLoad = Result<(usize, Vec<(usize, Event)>, Vec<BadJsonLine>), std::io::Error>;
+
+fn load_events_from_jsonl(path: &Path) -> JsonlEventsLoad {
let text = std::fs::read_to_string(path)?;
let total_lines = text.lines().count();
let mut events = Vec::new();
diff --git a/server/src/resolvers/github.rs b/server/src/resolvers/github.rs
index 30dd4cdac96de0e3da4fa03fdf82f91bed73bfe0..3cec5e8b7249a4597c378890cc9e6125104e76c3 100644
--- a/server/src/resolvers/github.rs
+++ b/server/src/resolvers/github.rs
@@ -749,6 +749,6 @@ mod tests {
GithubImportKind::Issue,
);
assert!(card.sublines.iter().any(|l| l.contains("@octo")));
- assert_eq!(card.excerpt.as_deref(), Some("The issue body.").as_deref());
+ assert_eq!(card.excerpt.as_deref(), Some("The issue body."));
}
}
diff --git a/server/tests/basic.rs b/server/tests/basic.rs
index 31a35251a8abd6f4a48c1d7782b6985f621375e1..9d83e7a97e2c7494790db17c4b5b30c181705026 100644
--- a/server/tests/basic.rs
+++ b/server/tests/basic.rs
@@ -337,7 +337,7 @@ async fn event_log_handles_corrupt_lines() {
.unwrap();
// Add empty line.
- writeln!(f, "").unwrap();
+ writeln!(f).unwrap();
let (loaded, bad) = log.load_all().await.unwrap();
assert_eq!(loaded.len(), 2);
@@ -511,9 +511,7 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() {
"~/t/a {a}\n~/t/b {b}\n{zero}\n~/t/a 0:0 ~/t/b\n",
)
.expect_err("0:0 vote must be rejected by the parser");
- let msg = match err {
- slugsocial_server::dsl::DslError::Parse(m) => m,
- };
+ let slugsocial_server::dsl::DslError::Parse(msg) = err;
assert!(
msg.contains("0:0"),
"expected message about invalid 0:0 ratio, got: {msg}"
@@ -904,7 +902,7 @@ fn posts_by_actor_indexes_and_profile_visibility() {
fn feed_query(state: &ReducerState, cutoff: i64, limit: usize) -> (usize, Vec<String>) {
let matching: Vec<&str> = state.ingests_ordered.iter().rev()
.map(|id| id.as_str())
- .take_while(|id| state.ingests_by_id.get(*id).map_or(false, |ing| ing.ts > cutoff))
+ .take_while(|id| state.ingests_by_id.get(*id).is_some_and(|ing| ing.ts > cutoff))
.filter(|id| {
state.ingests_by_id.get(*id).is_some_and(|ing| {
let scope = slugsocial_server::reducer::scope_from_room_wire(&ing.room_id);
diff --git a/server/tests/integration_health.rs b/server/tests/integration_health.rs
index 481222d8c48c44fcfb7e9ba26cf7644b5c26d5f4..351aae6b0e9738021886a076ee08fab5cf5a0001 100644
--- a/server/tests/integration_health.rs
+++ b/server/tests/integration_health.rs
@@ -7,7 +7,7 @@ async fn test_healthz() {
let (addr, _tmp, _log, _handle) = create_test_server().await;
let client = reqwest::Client::new();
let response = client
- .get(&format!("http://{}/healthz", addr))
+ .get(format!("http://{}/healthz", addr))
.send()
.await
.unwrap();
diff --git a/server/tests/integration_rpc.rs b/server/tests/integration_rpc.rs
index ccd89a02594bd2a8e7047edafca04b0e54384139..ec446d94a38c8d6961d2135dd031da7ab2a39b48 100644
--- a/server/tests/integration_rpc.rs
+++ b/server/tests/integration_rpc.rs
@@ -440,7 +440,6 @@ async fn test_rank_history() {
let bearer = test_bearer();
let ingest = |delegate: &str, text: &str| {
let client = client.clone();
- let addr = addr;
let bearer = bearer.clone();
let text = text.to_string();
let delegate = delegate.to_string();
diff --git a/server/tests/support/mod.rs b/server/tests/support/mod.rs
index 84dc1d68b6c1e1fdfdbd0757139c06c3daf1e9a6..4a620eaa875e7a1145f2a4e82cc4ff2be21d5345 100644
--- a/server/tests/support/mod.rs
+++ b/server/tests/support/mod.rs
@@ -1,3 +1,6 @@
+//! Shared helpers for integration tests; each test binary uses a different subset.
+#![allow(dead_code)]
+
use sha2::{Digest, Sha256};
use slugsocial_server::{
event_log::EventLog,
diff --git a/types/src/paths.rs b/types/src/paths.rs
index ebc299c5e1af456c3e4b7fa45ed9fb23313e88c4..d2e799942b98dd7342764475deb17903fb60c600 100644
--- a/types/src/paths.rs
+++ b/types/src/paths.rs
@@ -411,7 +411,7 @@ mod tests {
#[test]
fn garden_item_url_deref_to_str() {
let g = GardenItemUrl::from_storage_str("https://slug.social/~/x", "public");
- let s: &str = &*g;
+ let s: &str = &g;
assert_eq!(s, "https://slug.social/~/x");
}
B — c_6a02ffb06a41 (tommy-mor)
message
[3bc88847] removed optional
diff preview
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index ba93ca87182d49f57dffc8220f604ffa150f9a6c..5049a26b096bd8435d6eb9e75ccb751b6f489061 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -1337,8 +1337,7 @@ pub async fn handle_rpc_batch(
.ingests_by_scope_thread
.get(&(scope.clone(), e.thread.clone()))
.and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
- .map(|i| i + 1)
- .unwrap_or(0);
+ .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
RankHistoryRow {
ts: e.ts,
scope_rank: e.scope_rank,
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 319feb15a6b68d2b5df98b4289fedbc9bdd048d3..23245d6fdfc9019b199ab8417150faf5f3297067 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -450,6 +450,7 @@ struct RankHistoryEntryView {
scope_total: usize,
scope_rank_delta: i32,
thread: String,
+ /// 0-based index as [`crate::html::forum::ingest::thread_post_index_in_scope`] / `/t/tag/N`.
thread_post_index: usize,
caused_by: Vec<crate::reducer::VoteData>,
}
@@ -573,11 +574,11 @@ fn build_rank_history(
})
.unwrap_or_default();
- let thread_post_index = reduced.ingests_by_scope_thread
+ let thread_post_index = reduced
+ .ingests_by_scope_thread
.get(&(scope.clone(), e.thread.clone()))
.and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
- .map(|i| i + 1)
- .unwrap_or(0);
+ .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
RankHistoryEntryView {
ts: e.ts,
@@ -704,11 +705,9 @@ async fn render_scope_view(
span class="muted" { (ago) (label) }
" · "
a href=(thread_href(&e.thread)) { "#" (e.thread) }
- @if e.thread_post_index > 0 {
- " "
- a href=(format!("{}/{}", thread_href(&e.thread), e.thread_post_index)) {
- span class="muted" { "post #" (e.thread_post_index) }
- }
+ " "
+ a href=(format!("{}/{}", thread_href(&e.thread), e.thread_post_index)) {
+ span class="muted" { "post #" (e.thread_post_index) }
}
}
@if e.caused_by.is_empty() {
diff --git a/server/tests/integration.rs b/server/tests/integration.rs
index d4c5bfe9c6f1c71dc61878bd8c5e729b1b7c69ef..9766adb43ad315a64f5df17b79f66718ce149509 100644
--- a/server/tests/integration.rs
+++ b/server/tests/integration.rs
@@ -1322,6 +1322,11 @@ async fn test_rank_history() {
assert_eq!(entry["scope_rank_delta"], 0, "delta is 0 on first appearance");
let caused_by = entry["caused_by"].as_array().unwrap();
assert_eq!(caused_by.len(), 2, "both votes in the ingest touched rust");
+ assert_eq!(
+ entry["thread_post_index"],
+ 0,
+ "rank history links use same 0-based index as /t/hist-test/0"
+ );
ingest(
"00000000-0000-0000-0000-000000000002:rig:test/model",
@@ -1349,6 +1354,16 @@ async fn test_rank_history() {
assert_eq!(caused_by2.len(), 1);
assert!(caused_by2[0]["a"].as_str().unwrap().ends_with("python") ||
caused_by2[0]["b"].as_str().unwrap().ends_with("python"));
+ assert_eq!(
+ hist2[0]["thread_post_index"],
+ 0,
+ "first hist-test post is chronological index 0"
+ );
+ assert_eq!(
+ hist2[1]["thread_post_index"],
+ 1,
+ "second ingest is chronological index 1"
+ );
let hist_rust2 = rpc_batch(
&client,
diff --git a/types/src/lib.rs b/types/src/lib.rs
index edbb923e4d7d41ad82dfc254c3bd697562383527..49abba8ea9f786ef68e3157d2a5d309e15e09ba0 100644
--- a/types/src/lib.rs
+++ b/types/src/lib.rs
@@ -253,7 +253,7 @@ pub struct FeedPost {
/// Primary thread tag (without #), if the ingest declared one.
#[serde(skip_serializing_if = "Option::is_none")]
pub thread: Option<String>,
- /// 1-indexed chronological position of this post within the thread.
+ /// 1-based display ordinal for this post within the thread (feed only; URLs use 0-based paths).
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_post_index: Option<usize>,
/// Full raw body of the ingest document.
@@ -628,7 +628,7 @@ pub struct RankHistoryRow {
pub score: f64,
/// Thread tag of the ingest that triggered this rank change.
pub thread: String,
- /// 1-indexed chronological position of this post within the thread.
+ /// 0-indexed chronological position of this post within the thread (same as `/t/tag/N` routes).
pub thread_post_index: usize,
/// Votes from this ingest that directly touched this item. Empty when change was transitive.
pub caused_by: Vec<VoteRow>,
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.