diff --git a/src/cli/commands/me/me_tests.rs b/src/cli/commands/me/me_tests.rs index 54f7808..0e0feb1 100644 --- a/src/cli/commands/me/me_tests.rs +++ b/src/cli/commands/me/me_tests.rs @@ -476,7 +476,7 @@ fn activity_note_on_assigned_issue() { let t = now_ms() - 1000; insert_note_at(&conn, 200, disc_id, 1, "bob", false, "a comment", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::Note); assert_eq!(results[0].entity_iid, 42); @@ -495,7 +495,7 @@ fn activity_note_on_authored_mr() { let t = now_ms() - 1000; insert_note_at(&conn, 200, disc_id, 1, "bob", false, "nice work", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::Note); assert_eq!(results[0].entity_type, "mr"); @@ -512,7 +512,7 @@ fn activity_state_event_on_my_issue() { let t = now_ms() - 1000; insert_state_event(&conn, 300, 1, Some(10), None, "closed", "bob", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::StatusChange); assert_eq!(results[0].summary, "closed"); @@ -528,7 +528,7 @@ fn activity_label_event_on_my_issue() { let t = now_ms() - 1000; insert_label_event(&conn, 400, 1, Some(10), None, "add", "bug", "bob", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::LabelChange); assert!(results[0].summary.contains("bug")); @@ -546,7 +546,7 @@ fn activity_excludes_unassociated_items() { let t = now_ms() - 1000; insert_note_at(&conn, 200, disc_id, 1, "bob", false, "a comment", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert!( results.is_empty(), "should not see activity on unassigned issues" @@ -578,13 +578,39 @@ fn activity_since_filter() { // since = 50 seconds ago, should only get the recent note let since = now_ms() - 50_000; - let results = query_activity(&conn, "alice", &[], since).unwrap(); + let results = query_activity(&conn, "alice", &[], since, false).unwrap(); assert_eq!(results.len(), 1); // Notes no longer duplicate body into body_preview (summary carries the content) assert_eq!(results[0].body_preview, None); assert_eq!(results[0].summary, "new comment"); } +#[test] +fn activity_full_body_flag() { + let conn = setup_test_db(); + insert_project(&conn, 1, "group/repo"); + insert_issue(&conn, 10, 1, 42, "someone"); + insert_assignee(&conn, 10, "alice"); + + let disc_id = 100; + insert_discussion(&conn, disc_id, 1, None, Some(10)); + let t = now_ms() - 1000; + // Create a body longer than 200 characters + let long_body = "a".repeat(300); + insert_note_at(&conn, 200, disc_id, 1, "bob", false, &long_body, t); + + // Without --full, summary should be truncated to 200 chars + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].summary.len(), 200); + + // With --full, summary should contain the full body + let results = query_activity(&conn, "alice", &[], 0, true).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].summary.len(), 300); + assert_eq!(results[0].summary, long_body); +} + #[test] fn activity_project_filter() { let conn = setup_test_db(); @@ -604,7 +630,7 @@ fn activity_project_filter() { insert_note_at(&conn, 201, disc_b, 2, "bob", false, "comment b", t); // Filter to project 1 only - let results = query_activity(&conn, "alice", &[1], 0).unwrap(); + let results = query_activity(&conn, "alice", &[1], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].project_path, "group/repo-a"); } @@ -623,7 +649,7 @@ fn activity_sorted_newest_first() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "first", t1); insert_note_at(&conn, 201, disc_id, 1, "charlie", false, "second", t2); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 2); assert!( results[0].timestamp >= results[1].timestamp, @@ -643,7 +669,7 @@ fn activity_is_own_flag() { let t = now_ms() - 1000; insert_note_at(&conn, 200, disc_id, 1, "alice", false, "my comment", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert!(results[0].is_own); } @@ -662,7 +688,7 @@ fn activity_assignment_system_note() { let t = now_ms() - 1000; insert_note_at(&conn, 200, disc_id, 1, "bob", true, "assigned to @alice", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::Assign); } @@ -679,7 +705,7 @@ fn activity_unassignment_system_note() { let t = now_ms() - 1000; insert_note_at(&conn, 200, disc_id, 1, "bob", true, "unassigned @alice", t); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::Unassign); } @@ -705,7 +731,7 @@ fn activity_review_request_system_note() { t, ); - let results = query_activity(&conn, "alice", &[], 0).unwrap(); + let results = query_activity(&conn, "alice", &[], 0, false).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].event_type, ActivityEventType::ReviewRequest); } @@ -731,7 +757,7 @@ fn since_last_check_detects_mention_with_trailing_comma() { t, ); - let groups = query_since_last_check(&conn, "alice", 0).unwrap(); + let groups = query_since_last_check(&conn, "alice", 0, false).unwrap(); let total_events: usize = groups.iter().map(|g| g.events.len()).sum(); assert_eq!(total_events, 1, "expected mention with comma to match"); } @@ -755,7 +781,7 @@ fn since_last_check_ignores_email_like_text() { t, ); - let groups = query_since_last_check(&conn, "alice", 0).unwrap(); + let groups = query_since_last_check(&conn, "alice", 0, false).unwrap(); let total_events: usize = groups.iter().map(|g| g.events.len()).sum(); assert_eq!(total_events, 0, "email text should not count as mention"); } @@ -779,7 +805,7 @@ fn since_last_check_detects_mention_with_trailing_period() { t, ); - let groups = query_since_last_check(&conn, "alice", 0).unwrap(); + let groups = query_since_last_check(&conn, "alice", 0, false).unwrap(); let total_events: usize = groups.iter().map(|g| g.events.len()).sum(); assert_eq!(total_events, 1, "expected mention with period to match"); } @@ -803,7 +829,7 @@ fn since_last_check_detects_mention_inside_parentheses() { t, ); - let groups = query_since_last_check(&conn, "alice", 0).unwrap(); + let groups = query_since_last_check(&conn, "alice", 0, false).unwrap(); let total_events: usize = groups.iter().map(|g| g.events.len()).sum(); assert_eq!(total_events, 1, "expected parenthesized mention to match"); } @@ -827,7 +853,7 @@ fn since_last_check_ignores_domain_like_text() { t, ); - let groups = query_since_last_check(&conn, "alice", 0).unwrap(); + let groups = query_since_last_check(&conn, "alice", 0, false).unwrap(); let total_events: usize = groups.iter().map(|g| g.events.len()).sum(); assert_eq!( total_events, 0, @@ -835,6 +861,33 @@ fn since_last_check_ignores_domain_like_text() { ); } +#[test] +fn since_last_check_full_body_flag() { + let conn = setup_test_db(); + insert_project(&conn, 1, "group/repo"); + insert_issue(&conn, 10, 1, 42, "someone"); + insert_assignee(&conn, 10, "alice"); + let disc_id = 100; + insert_discussion(&conn, disc_id, 1, None, Some(10)); + let t = now_ms() - 1000; + // Create a body longer than 200 characters + let long_body = "b".repeat(300); + insert_note_at(&conn, 200, disc_id, 1, "bob", false, &long_body, t); + + // Without --full, summary should be truncated to 200 chars + let groups = query_since_last_check(&conn, "alice", 0, false).unwrap(); + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].events.len(), 1); + assert_eq!(groups[0].events[0].summary.len(), 200); + + // With --full, summary should contain the full body + let groups = query_since_last_check(&conn, "alice", 0, true).unwrap(); + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].events.len(), 1); + assert_eq!(groups[0].events[0].summary.len(), 300); + assert_eq!(groups[0].events[0].summary, long_body); +} + // ─── Helper Tests ────────────────────────────────────────────────────────── #[test] diff --git a/src/cli/commands/me/mod.rs b/src/cli/commands/me/mod.rs index a7ebceb..bba8bcf 100644 --- a/src/cli/commands/me/mod.rs +++ b/src/cli/commands/me/mod.rs @@ -147,7 +147,7 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> { }; let activity = if want_activity { - query_activity(&conn, username, &project_ids, since_ms)? + query_activity(&conn, username, &project_ids, since_ms, args.full)? } else { Vec::new() }; @@ -158,7 +158,7 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> { // permanently skip events from other projects. let mut global_watermark: Option = None; let since_last_check = if let Some(prev_cursor) = cursor_ms { - let groups = query_since_last_check(&conn, username, prev_cursor)?; + let groups = query_since_last_check(&conn, username, prev_cursor, args.full)?; // Watermark from ALL groups (unfiltered) — this is the true high-water mark global_watermark = groups.iter().map(|g| g.latest_timestamp).max(); // If --project was passed, filter groups by project for display only @@ -318,6 +318,7 @@ mod tests { all: false, user: user.map(String::from), fields: None, + full: false, reset_cursor: false, } } diff --git a/src/cli/commands/me/queries.rs b/src/cli/commands/me/queries.rs index a76de4f..30ec934 100644 --- a/src/cli/commands/me/queries.rs +++ b/src/cli/commands/me/queries.rs @@ -266,11 +266,14 @@ pub fn query_reviewing_mrs( /// Query activity events on items currently associated with the user. /// Combines notes, state events, label events, milestone events, and /// assignment/reviewer system notes into a unified feed sorted newest-first. +/// +/// When `full_body` is true, note content is returned in full without truncation. pub fn query_activity( conn: &Connection, username: &str, project_ids: &[i64], since_ms: i64, + full_body: bool, ) -> Result> { // Build project filter for activity sources. // Activity params: ?1=username, ?2=since_ms, ?3+=project_ids @@ -292,6 +295,13 @@ pub fn query_activity( WHERE rv.merge_request_id = {entity_mr_id} AND rv.username = ?1 AND mr3.state = 'opened') )"; + // Body selection: truncate to 200 chars unless --full was passed + let body_expr = if full_body { + "n.body" + } else { + "SUBSTR(n.body, 1, 200)" + }; + // Source 1: Human comments on my items let notes_sql = format!( "SELECT n.created_at, 'note', @@ -300,7 +310,7 @@ pub fn query_activity( p.path_with_namespace, n.author_username, CASE WHEN n.author_username = ?1 THEN 1 ELSE 0 END, - SUBSTR(n.body, 1, 200), + {body_expr}, NULL FROM notes n JOIN discussions d ON n.discussion_id = d.id @@ -492,10 +502,13 @@ struct RawSinceCheckRow { /// 1. Others' comments on my open items /// 2. @mentions on any item (not restricted to my items) /// 3. Assignment/review-request system notes mentioning me +/// +/// When `full_body` is true, note content is returned in full without truncation. pub fn query_since_last_check( conn: &Connection, username: &str, cursor_ms: i64, + full_body: bool, ) -> Result> { // Build the "my items" subquery fragments (reused from activity). let my_issue_check = "EXISTS ( @@ -510,6 +523,13 @@ pub fn query_since_last_check( WHERE rv.merge_request_id = {entity_mr_id} AND rv.username = ?1 AND mr3.state = 'opened') )"; + // Body selection: truncate to 200 chars unless --full was passed + let body_expr = if full_body { + "n.body" + } else { + "SUBSTR(n.body, 1, 200)" + }; + // Source 1: Others' comments on my open items let source1 = format!( "SELECT n.created_at, 'note', @@ -518,7 +538,7 @@ pub fn query_since_last_check( COALESCE(i.title, m.title), p.path_with_namespace, n.author_username, - SUBSTR(n.body, 1, 200), + {body_expr}, NULL, 0, NULL @@ -547,7 +567,7 @@ pub fn query_since_last_check( COALESCE(i.title, m.title), p.path_with_namespace, n.author_username, - SUBSTR(n.body, 1, 200), + {body_expr}, NULL, 1, n.body diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9547ec7..fc15004 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1123,6 +1123,10 @@ pub struct MeArgs { #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, + /// Show full note content (no truncation) + #[arg(long, help_heading = "Output")] + pub full: bool, + /// Reset the since-last-check cursor (next run shows no new events) #[arg(long, help_heading = "Output")] pub reset_cursor: bool,