feat(me): add 30-day mention age cutoff to filter stale @-mentions
Previously, query_mentioned_in returned mentions from any time in the entity's history as long as the entity was still open (or recently closed). This caused noise: a mention from 6 months ago on a still-open issue would appear in the dashboard indefinitely. Now the SQL filters notes by created_at > mention_cutoff_ms, defaulting to 30 days. The recency_cutoff (7 days) still governs closed/merged entity visibility — this new cutoff governs mention note age on open entities. Signature change: query_mentioned_in gains a mention_cutoff_ms parameter. All existing test call sites updated. Two new tests verify the boundary: - mentioned_in_excludes_old_mention_on_open_issue (45-day mention filtered) - mentioned_in_includes_recent_mention_on_open_issue (5-day mention kept)
This commit is contained in:
@@ -946,7 +946,7 @@ fn mentioned_in_finds_mention_on_unassigned_issue() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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.len(), 1);
|
||||||
assert_eq!(results[0].entity_type, "issue");
|
assert_eq!(results[0].entity_type, "issue");
|
||||||
assert_eq!(results[0].iid, 42);
|
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);
|
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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");
|
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);
|
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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");
|
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);
|
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "cc @alice", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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.len(), 1);
|
||||||
assert_eq!(results[0].entity_type, "mr");
|
assert_eq!(results[0].entity_type, "mr");
|
||||||
assert_eq!(results[0].iid, 99);
|
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);
|
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "@alice thoughts?", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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");
|
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);
|
insert_note_at(&conn, 200, disc_id, 1, "charlie", false, "@alice fyi", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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!(
|
assert!(
|
||||||
results.is_empty(),
|
results.is_empty(),
|
||||||
"should exclude MRs where user is reviewer"
|
"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);
|
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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.len(), 1, "recently closed issue should be included");
|
||||||
assert_eq!(results[0].state, "closed");
|
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);
|
insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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");
|
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
|
// alice has NOT replied
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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.len(), 1);
|
||||||
assert_eq!(results[0].attention_state, AttentionState::NeedsAttention);
|
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);
|
insert_note_at(&conn, 201, disc_id, 1, "alice", false, "looks good", t2);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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.len(), 1);
|
||||||
assert_eq!(results[0].attention_state, AttentionState::AwaitingResponse);
|
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);
|
insert_note_at(&conn, 201, disc_b, 2, "bob", false, "@alice", t);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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.len(), 1);
|
||||||
assert_eq!(results[0].project_path, "group/repo-a");
|
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);
|
insert_note_at(&conn, 201, disc_id, 1, "charlie", false, "@alice +1", t2);
|
||||||
|
|
||||||
let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000;
|
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");
|
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 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");
|
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 ──────────────────────────────────────────────────────────
|
// ─── Helper Tests ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ const DEFAULT_ACTIVITY_SINCE_DAYS: i64 = 1;
|
|||||||
const MS_PER_DAY: i64 = 24 * 60 * 60 * 1000;
|
const MS_PER_DAY: i64 = 24 * 60 * 60 * 1000;
|
||||||
/// Recency window for closed/merged items in the "Mentioned In" section: 7 days.
|
/// Recency window for closed/merged items in the "Mentioned In" section: 7 days.
|
||||||
const RECENCY_WINDOW_MS: i64 = 7 * MS_PER_DAY;
|
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.
|
/// 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 mentioned_in = if want_mentions {
|
||||||
let recency_cutoff = crate::core::time::now_ms() - RECENCY_WINDOW_MS;
|
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 {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
LEFT JOIN note_ts_issue nt ON nt.issue_id = ci.id
|
||||||
WHERE n.is_system = 0
|
WHERE n.is_system = 0
|
||||||
AND n.author_username != ?1
|
AND n.author_username != ?1
|
||||||
|
AND n.created_at > ?3
|
||||||
AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%'
|
AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%'
|
||||||
UNION ALL
|
UNION ALL
|
||||||
-- MR mentions (scoped to candidate entities only)
|
-- 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
|
LEFT JOIN note_ts_mr nt ON nt.merge_request_id = cm.id
|
||||||
WHERE n.is_system = 0
|
WHERE n.is_system = 0
|
||||||
AND n.author_username != ?1
|
AND n.author_username != ?1
|
||||||
|
AND n.created_at > ?3
|
||||||
AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%'
|
AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%'
|
||||||
ORDER BY 6 DESC
|
ORDER BY 6 DESC
|
||||||
LIMIT 500",
|
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.
|
/// Query issues and MRs where the user is @mentioned but not assigned/authored/reviewing.
|
||||||
///
|
///
|
||||||
/// Includes open items unconditionally, plus recently-closed/merged items
|
/// 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.
|
/// Returns deduplicated results sorted by attention priority then recency.
|
||||||
pub fn query_mentioned_in(
|
pub fn query_mentioned_in(
|
||||||
@@ -879,14 +882,16 @@ pub fn query_mentioned_in(
|
|||||||
username: &str,
|
username: &str,
|
||||||
project_ids: &[i64],
|
project_ids: &[i64],
|
||||||
recency_cutoff_ms: i64,
|
recency_cutoff_ms: i64,
|
||||||
|
mention_cutoff_ms: i64,
|
||||||
) -> Result<Vec<MeMention>> {
|
) -> Result<Vec<MeMention>> {
|
||||||
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.
|
// Materialized CTEs avoid pathological query plans for project-scoped mentions.
|
||||||
let sql = build_mentioned_in_sql(&project_clause);
|
let sql = build_mentioned_in_sql(&project_clause);
|
||||||
|
|
||||||
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||||
params.push(Box::new(username.to_string()));
|
params.push(Box::new(username.to_string()));
|
||||||
params.push(Box::new(recency_cutoff_ms));
|
params.push(Box::new(recency_cutoff_ms));
|
||||||
|
params.push(Box::new(mention_cutoff_ms));
|
||||||
for &pid in project_ids {
|
for &pid in project_ids {
|
||||||
params.push(Box::new(pid));
|
params.push(Box::new(pid));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user