diff --git a/src/cli/commands/me/me_tests.rs b/src/cli/commands/me/me_tests.rs index 99cf109..1150d5e 100644 --- a/src/cli/commands/me/me_tests.rs +++ b/src/cli/commands/me/me_tests.rs @@ -946,7 +946,7 @@ fn mentioned_in_finds_mention_on_unassigned_issue() { ); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].entity_type, "issue"); assert_eq!(results[0].iid, 42); @@ -964,7 +964,7 @@ fn mentioned_in_excludes_assigned_issue() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert!(results.is_empty(), "should exclude assigned issues"); } @@ -979,7 +979,7 @@ fn mentioned_in_excludes_authored_issue() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert!(results.is_empty(), "should exclude authored issues"); } @@ -995,7 +995,7 @@ fn mentioned_in_finds_mention_on_non_authored_mr() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "cc @alice", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].entity_type, "mr"); assert_eq!(results[0].iid, 99); @@ -1012,7 +1012,7 @@ fn mentioned_in_excludes_authored_mr() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "@alice thoughts?", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert!(results.is_empty(), "should exclude authored MRs"); } @@ -1028,7 +1028,7 @@ fn mentioned_in_excludes_reviewer_mr() { insert_note_at(&conn, 200, disc_id, 1, "charlie", false, "@alice fyi", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert!( results.is_empty(), "should exclude MRs where user is reviewer" @@ -1052,7 +1052,7 @@ fn mentioned_in_includes_recently_closed_issue() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1, "recently closed issue should be included"); assert_eq!(results[0].state, "closed"); } @@ -1074,7 +1074,7 @@ fn mentioned_in_excludes_old_closed_issue() { insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert!(results.is_empty(), "old closed issue should be excluded"); } @@ -1099,7 +1099,7 @@ fn mentioned_in_attention_needs_attention_when_unreplied() { // alice has NOT replied let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].attention_state, AttentionState::NeedsAttention); } @@ -1126,7 +1126,7 @@ fn mentioned_in_attention_awaiting_when_replied() { insert_note_at(&conn, 201, disc_id, 1, "alice", false, "looks good", t2); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].attention_state, AttentionState::AwaitingResponse); } @@ -1147,7 +1147,7 @@ fn mentioned_in_project_filter() { insert_note_at(&conn, 201, disc_b, 2, "bob", false, "@alice", t); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[1], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[1], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].project_path, "group/repo-a"); } @@ -1166,7 +1166,7 @@ fn mentioned_in_deduplicates_multiple_mentions_same_entity() { insert_note_at(&conn, 201, disc_id, 1, "charlie", false, "@alice +1", t2); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert_eq!(results.len(), 1, "should deduplicate to one entity"); } @@ -1190,10 +1190,47 @@ fn mentioned_in_rejects_false_positive_email() { ); let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; - let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, 0).unwrap(); assert!(results.is_empty(), "email-like text should not match"); } +#[test] +fn mentioned_in_excludes_old_mention_on_open_issue() { + let conn = setup_test_db(); + insert_project(&conn, 1, "group/repo"); + insert_issue(&conn, 10, 1, 42, "someone"); + let disc_id = 100; + insert_discussion(&conn, disc_id, 1, None, Some(10)); + // Mention from 45 days ago — outside 30-day mention window + let t = now_ms() - 45 * 24 * 3600 * 1000; + insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); + + let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; + let mention_cutoff = now_ms() - 30 * 24 * 3600 * 1000; + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, mention_cutoff).unwrap(); + assert!( + results.is_empty(), + "mentions older than 30 days should be excluded" + ); +} + +#[test] +fn mentioned_in_includes_recent_mention_on_open_issue() { + let conn = setup_test_db(); + insert_project(&conn, 1, "group/repo"); + insert_issue(&conn, 10, 1, 42, "someone"); + let disc_id = 100; + insert_discussion(&conn, disc_id, 1, None, Some(10)); + // Mention from 5 days ago — within 30-day window + let t = now_ms() - 5 * 24 * 3600 * 1000; + insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); + + let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; + let mention_cutoff = now_ms() - 30 * 24 * 3600 * 1000; + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff, mention_cutoff).unwrap(); + assert_eq!(results.len(), 1, "recent mentions should be included"); +} + // ─── Helper Tests ────────────────────────────────────────────────────────── #[test] diff --git a/src/cli/commands/me/mod.rs b/src/cli/commands/me/mod.rs index a60c7b2..f9ca66d 100644 --- a/src/cli/commands/me/mod.rs +++ b/src/cli/commands/me/mod.rs @@ -27,6 +27,8 @@ const DEFAULT_ACTIVITY_SINCE_DAYS: i64 = 1; const MS_PER_DAY: i64 = 24 * 60 * 60 * 1000; /// Recency window for closed/merged items in the "Mentioned In" section: 7 days. const RECENCY_WINDOW_MS: i64 = 7 * MS_PER_DAY; +/// Only show mentions from notes created within this window (30 days). +const MENTION_WINDOW_MS: i64 = 30 * MS_PER_DAY; /// Resolve the effective username from CLI flag or config. /// @@ -151,7 +153,14 @@ pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> { let mentioned_in = if want_mentions { let recency_cutoff = crate::core::time::now_ms() - RECENCY_WINDOW_MS; - query_mentioned_in(&conn, username, &project_ids, recency_cutoff)? + let mention_cutoff = crate::core::time::now_ms() - MENTION_WINDOW_MS; + query_mentioned_in( + &conn, + username, + &project_ids, + recency_cutoff, + mention_cutoff, + )? } else { Vec::new() }; diff --git a/src/cli/commands/me/queries.rs b/src/cli/commands/me/queries.rs index 8b72cce..5581c53 100644 --- a/src/cli/commands/me/queries.rs +++ b/src/cli/commands/me/queries.rs @@ -849,6 +849,7 @@ fn build_mentioned_in_sql(project_clause: &str) -> String { LEFT JOIN note_ts_issue nt ON nt.issue_id = ci.id WHERE n.is_system = 0 AND n.author_username != ?1 + AND n.created_at > ?3 AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%' UNION ALL -- MR mentions (scoped to candidate entities only) @@ -862,6 +863,7 @@ fn build_mentioned_in_sql(project_clause: &str) -> String { LEFT JOIN note_ts_mr nt ON nt.merge_request_id = cm.id WHERE n.is_system = 0 AND n.author_username != ?1 + AND n.created_at > ?3 AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%' ORDER BY 6 DESC LIMIT 500", @@ -871,7 +873,8 @@ fn build_mentioned_in_sql(project_clause: &str) -> String { /// Query issues and MRs where the user is @mentioned but not assigned/authored/reviewing. /// /// Includes open items unconditionally, plus recently-closed/merged items -/// (where `updated_at > recency_cutoff_ms`). +/// (where `updated_at > recency_cutoff_ms`). Only considers mentions in notes +/// created after `mention_cutoff_ms` (typically 30 days ago). /// /// Returns deduplicated results sorted by attention priority then recency. pub fn query_mentioned_in( @@ -879,14 +882,16 @@ pub fn query_mentioned_in( username: &str, project_ids: &[i64], recency_cutoff_ms: i64, + mention_cutoff_ms: i64, ) -> Result> { - let project_clause = build_project_clause_at("p.id", project_ids, 3); + let project_clause = build_project_clause_at("p.id", project_ids, 4); // Materialized CTEs avoid pathological query plans for project-scoped mentions. let sql = build_mentioned_in_sql(&project_clause); let mut params: Vec> = Vec::new(); params.push(Box::new(username.to_string())); params.push(Box::new(recency_cutoff_ms)); + params.push(Box::new(mention_cutoff_ms)); for &pid in project_ids { params.push(Box::new(pid)); }