feat(who): filter unresolved discussions to open entities only
Workload and active modes now exclude discussions on closed issues and merged/closed MRs by default. Adds --include-closed flag to restore the previous behavior when needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,15 +54,27 @@ fn insert_mr(conn: &Connection, id: i64, project_id: i64, iid: i64, author: &str
|
||||
}
|
||||
|
||||
fn insert_issue(conn: &Connection, id: i64, project_id: i64, iid: i64, author: &str) {
|
||||
insert_issue_with_state(conn, id, project_id, iid, author, "opened");
|
||||
}
|
||||
|
||||
fn insert_issue_with_state(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
project_id: i64,
|
||||
iid: i64,
|
||||
author: &str,
|
||||
state: &str,
|
||||
) {
|
||||
conn.execute(
|
||||
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, author_username, created_at, updated_at, last_seen_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 'opened', ?6, ?7, ?8, ?9)",
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
rusqlite::params![
|
||||
id,
|
||||
id * 10,
|
||||
project_id,
|
||||
iid,
|
||||
format!("Issue {iid}"),
|
||||
state,
|
||||
author,
|
||||
now_ms(),
|
||||
now_ms(),
|
||||
@@ -134,6 +146,24 @@ fn insert_diffnote(
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn insert_note(conn: &Connection, id: i64, discussion_id: i64, project_id: i64, author: &str) {
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, note_type, is_system, author_username, body, created_at, updated_at, last_seen_at)
|
||||
VALUES (?1, ?2, ?3, ?4, 'DiscussionNote', 0, ?5, 'comment', ?6, ?7, ?8)",
|
||||
rusqlite::params![
|
||||
id,
|
||||
id * 10,
|
||||
discussion_id,
|
||||
project_id,
|
||||
author,
|
||||
now_ms(),
|
||||
now_ms(),
|
||||
now_ms()
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn insert_assignee(conn: &Connection, issue_id: i64, username: &str) {
|
||||
conn.execute(
|
||||
"INSERT INTO issue_assignees (issue_id, username) VALUES (?1, ?2)",
|
||||
@@ -263,6 +293,7 @@ fn test_is_file_path_discrimination() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -286,6 +317,7 @@ fn test_is_file_path_discrimination() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -309,6 +341,7 @@ fn test_is_file_path_discrimination() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -332,6 +365,7 @@ fn test_is_file_path_discrimination() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -355,6 +389,7 @@ fn test_is_file_path_discrimination() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -378,6 +413,7 @@ fn test_is_file_path_discrimination() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -402,6 +438,7 @@ fn test_detail_rejected_outside_expert_mode() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
};
|
||||
let mode = resolve_mode(&args).unwrap();
|
||||
@@ -430,6 +467,7 @@ fn test_detail_allowed_in_expert_mode() {
|
||||
as_of: None,
|
||||
explain_score: false,
|
||||
include_bots: false,
|
||||
include_closed: false,
|
||||
all_history: false,
|
||||
};
|
||||
let mode = resolve_mode(&args).unwrap();
|
||||
@@ -579,7 +617,7 @@ fn test_workload_query() {
|
||||
insert_assignee(&conn, 1, "dev_a");
|
||||
insert_mr(&conn, 1, 1, 100, "dev_a", "opened");
|
||||
|
||||
let result = query_workload(&conn, "dev_a", None, None, 20).unwrap();
|
||||
let result = query_workload(&conn, "dev_a", None, None, 20, true).unwrap();
|
||||
assert_eq!(result.assigned_issues.len(), 1);
|
||||
assert_eq!(result.authored_mrs.len(), 1);
|
||||
}
|
||||
@@ -626,7 +664,7 @@ fn test_active_query() {
|
||||
// Second note by same participant -- note_count should be 2, participants still ["reviewer_b"]
|
||||
insert_diffnote(&conn, 2, 1, 1, "reviewer_b", "src/foo.rs", "follow-up");
|
||||
|
||||
let result = query_active(&conn, None, 0, 20).unwrap();
|
||||
let result = query_active(&conn, None, 0, 20, true).unwrap();
|
||||
assert_eq!(result.total_unresolved_in_window, 1);
|
||||
assert_eq!(result.discussions.len(), 1);
|
||||
assert_eq!(result.discussions[0].participants, vec!["reviewer_b"]);
|
||||
@@ -878,7 +916,7 @@ fn test_active_participants_sorted() {
|
||||
insert_diffnote(&conn, 1, 1, 1, "zebra_user", "src/foo.rs", "note 1");
|
||||
insert_diffnote(&conn, 2, 1, 1, "alpha_user", "src/foo.rs", "note 2");
|
||||
|
||||
let result = query_active(&conn, None, 0, 20).unwrap();
|
||||
let result = query_active(&conn, None, 0, 20, true).unwrap();
|
||||
assert_eq!(
|
||||
result.discussions[0].participants,
|
||||
vec!["alpha_user", "zebra_user"]
|
||||
@@ -3265,3 +3303,94 @@ fn test_deterministic_accumulation_order() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests: include_closed filter ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn workload_excludes_closed_entity_discussions() {
|
||||
let conn = setup_test_db();
|
||||
insert_project(&conn, 1, "group/repo");
|
||||
|
||||
// Open issue with unresolved discussion
|
||||
insert_issue_with_state(&conn, 10, 1, 10, "someone", "opened");
|
||||
insert_discussion(&conn, 100, 1, None, Some(10), true, false);
|
||||
insert_note(&conn, 1000, 100, 1, "alice");
|
||||
|
||||
// Closed issue with unresolved discussion
|
||||
insert_issue_with_state(&conn, 20, 1, 20, "someone", "closed");
|
||||
insert_discussion(&conn, 200, 1, None, Some(20), true, false);
|
||||
insert_note(&conn, 2000, 200, 1, "alice");
|
||||
|
||||
// Default: exclude closed
|
||||
let result = query_workload(&conn, "alice", None, None, 50, false).unwrap();
|
||||
assert_eq!(result.unresolved_discussions.len(), 1);
|
||||
assert_eq!(result.unresolved_discussions[0].entity_iid, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workload_include_closed_flag_shows_all() {
|
||||
let conn = setup_test_db();
|
||||
insert_project(&conn, 1, "group/repo");
|
||||
|
||||
insert_issue_with_state(&conn, 10, 1, 10, "someone", "opened");
|
||||
insert_discussion(&conn, 100, 1, None, Some(10), true, false);
|
||||
insert_note(&conn, 1000, 100, 1, "alice");
|
||||
|
||||
insert_issue_with_state(&conn, 20, 1, 20, "someone", "closed");
|
||||
insert_discussion(&conn, 200, 1, None, Some(20), true, false);
|
||||
insert_note(&conn, 2000, 200, 1, "alice");
|
||||
|
||||
let result = query_workload(&conn, "alice", None, None, 50, true).unwrap();
|
||||
assert_eq!(result.unresolved_discussions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workload_excludes_merged_mr_discussions() {
|
||||
let conn = setup_test_db();
|
||||
insert_project(&conn, 1, "group/repo");
|
||||
|
||||
// Open MR with unresolved discussion
|
||||
insert_mr(&conn, 10, 1, 10, "someone", "opened");
|
||||
insert_discussion(&conn, 100, 1, Some(10), None, true, false);
|
||||
insert_note(&conn, 1000, 100, 1, "alice");
|
||||
|
||||
// Merged MR with unresolved discussion
|
||||
insert_mr(&conn, 20, 1, 20, "someone", "merged");
|
||||
insert_discussion(&conn, 200, 1, Some(20), None, true, false);
|
||||
insert_note(&conn, 2000, 200, 1, "alice");
|
||||
|
||||
let result = query_workload(&conn, "alice", None, None, 50, false).unwrap();
|
||||
assert_eq!(result.unresolved_discussions.len(), 1);
|
||||
assert_eq!(result.unresolved_discussions[0].entity_iid, 10);
|
||||
|
||||
// include_closed shows both
|
||||
let result = query_workload(&conn, "alice", None, None, 50, true).unwrap();
|
||||
assert_eq!(result.unresolved_discussions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_excludes_closed_entity_discussions() {
|
||||
let conn = setup_test_db();
|
||||
insert_project(&conn, 1, "group/repo");
|
||||
|
||||
// Open issue with unresolved discussion
|
||||
insert_issue_with_state(&conn, 10, 1, 10, "someone", "opened");
|
||||
insert_discussion(&conn, 100, 1, None, Some(10), true, false);
|
||||
insert_note(&conn, 1000, 100, 1, "alice");
|
||||
|
||||
// Closed issue with unresolved discussion
|
||||
insert_issue_with_state(&conn, 20, 1, 20, "someone", "closed");
|
||||
insert_discussion(&conn, 200, 1, None, Some(20), true, false);
|
||||
insert_note(&conn, 2000, 200, 1, "alice");
|
||||
|
||||
// Default: exclude closed
|
||||
let result = query_active(&conn, None, 0, 50, false).unwrap();
|
||||
assert_eq!(result.discussions.len(), 1);
|
||||
assert_eq!(result.discussions[0].entity_iid, 10);
|
||||
assert_eq!(result.total_unresolved_in_window, 1);
|
||||
|
||||
// include_closed shows both
|
||||
let result = query_active(&conn, None, 0, 50, true).unwrap();
|
||||
assert_eq!(result.discussions.len(), 2);
|
||||
assert_eq!(result.total_unresolved_in_window, 2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user