Replace hardcoded truncation widths across CLI commands with render::flex_width() calls that adapt to terminal size. Remove server-side truncate_to_chars() in timeline collect/seed stages so full text is preserved through the pipeline — truncation now happens only at the presentation layer where terminal width is known. Affected commands: explain, file-history, list (issues/mrs/notes), me, timeline, who (active/expert/workload).
706 lines
22 KiB
Rust
706 lines
22 KiB
Rust
use super::*;
|
|
use crate::core::db::{create_connection, run_migrations};
|
|
use std::path::Path;
|
|
|
|
fn setup_test_db() -> Connection {
|
|
let conn = create_connection(Path::new(":memory:")).unwrap();
|
|
run_migrations(&conn).unwrap();
|
|
conn
|
|
}
|
|
|
|
fn insert_project(conn: &Connection) -> i64 {
|
|
conn.execute(
|
|
"INSERT INTO projects (gitlab_project_id, path_with_namespace, web_url) VALUES (1, 'group/project', 'https://gitlab.com/group/project')",
|
|
[],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
fn insert_issue(conn: &Connection, project_id: i64, iid: i64) -> i64 {
|
|
conn.execute(
|
|
"INSERT INTO issues (gitlab_id, project_id, iid, title, state, author_username, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, ?3, 'Auth bug', 'opened', 'alice', 1000, 2000, 3000, 'https://gitlab.com/group/project/-/issues/1')",
|
|
rusqlite::params![iid * 100, project_id, iid],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
fn insert_mr(conn: &Connection, project_id: i64, iid: i64, merged_at: Option<i64>) -> i64 {
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (gitlab_id, project_id, iid, title, state, author_username, created_at, updated_at, last_seen_at, merged_at, merge_user_username, web_url) VALUES (?1, ?2, ?3, 'Fix auth', 'merged', 'bob', 1000, 5000, 6000, ?4, 'charlie', 'https://gitlab.com/group/project/-/merge_requests/10')",
|
|
rusqlite::params![iid * 100, project_id, iid, merged_at],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
fn make_entity_ref(entity_type: &str, entity_id: i64, iid: i64) -> EntityRef {
|
|
EntityRef {
|
|
entity_type: entity_type.to_owned(),
|
|
entity_id,
|
|
entity_iid: iid,
|
|
project_path: "group/project".to_owned(),
|
|
}
|
|
}
|
|
|
|
fn insert_state_event(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
issue_id: Option<i64>,
|
|
mr_id: Option<i64>,
|
|
state: &str,
|
|
created_at: i64,
|
|
) {
|
|
let gitlab_id: i64 = rand::random::<u32>().into();
|
|
conn.execute(
|
|
"INSERT INTO resource_state_events (gitlab_id, project_id, issue_id, merge_request_id, state, actor_username, created_at) VALUES (?1, ?2, ?3, ?4, ?5, 'alice', ?6)",
|
|
rusqlite::params![gitlab_id, project_id, issue_id, mr_id, state, created_at],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn insert_label_event(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
issue_id: Option<i64>,
|
|
mr_id: Option<i64>,
|
|
action: &str,
|
|
label_name: Option<&str>,
|
|
created_at: i64,
|
|
) {
|
|
let gitlab_id: i64 = rand::random::<u32>().into();
|
|
conn.execute(
|
|
"INSERT INTO resource_label_events (gitlab_id, project_id, issue_id, merge_request_id, action, label_name, actor_username, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'alice', ?7)",
|
|
rusqlite::params![gitlab_id, project_id, issue_id, mr_id, action, label_name, created_at],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn insert_milestone_event(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
issue_id: Option<i64>,
|
|
mr_id: Option<i64>,
|
|
action: &str,
|
|
milestone_title: Option<&str>,
|
|
created_at: i64,
|
|
) {
|
|
let gitlab_id: i64 = rand::random::<u32>().into();
|
|
conn.execute(
|
|
"INSERT INTO resource_milestone_events (gitlab_id, project_id, issue_id, merge_request_id, action, milestone_title, actor_username, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'alice', ?7)",
|
|
rusqlite::params![gitlab_id, project_id, issue_id, mr_id, action, milestone_title, created_at],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_creation_event() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
assert_eq!(events.len(), 1);
|
|
assert!(matches!(events[0].event_type, TimelineEventType::Created));
|
|
assert_eq!(events[0].timestamp, 1000);
|
|
assert_eq!(events[0].actor, Some("alice".to_owned()));
|
|
assert!(events[0].is_seed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_state_events() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
insert_state_event(&conn, project_id, Some(issue_id), None, "closed", 3000);
|
|
insert_state_event(&conn, project_id, Some(issue_id), None, "reopened", 4000);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
|
|
// Created + 2 state changes = 3
|
|
assert_eq!(events.len(), 3);
|
|
assert!(matches!(events[0].event_type, TimelineEventType::Created));
|
|
assert!(matches!(
|
|
events[1].event_type,
|
|
TimelineEventType::StateChanged { ref state } if state == "closed"
|
|
));
|
|
assert!(matches!(
|
|
events[2].event_type,
|
|
TimelineEventType::StateChanged { ref state } if state == "reopened"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_merged_dedup() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let mr_id = insert_mr(&conn, project_id, 10, Some(5000));
|
|
|
|
// Also add a state event for 'merged' — this should NOT produce a StateChanged
|
|
insert_state_event(&conn, project_id, None, Some(mr_id), "merged", 5000);
|
|
|
|
let seeds = vec![make_entity_ref("merge_request", mr_id, 10)];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
|
|
// Should have Created + Merged (not Created + StateChanged{merged} + Merged)
|
|
let merged_count = events
|
|
.iter()
|
|
.filter(|e| matches!(e.event_type, TimelineEventType::Merged))
|
|
.count();
|
|
let state_merged_count = events
|
|
.iter()
|
|
.filter(|e| matches!(&e.event_type, TimelineEventType::StateChanged { state } if state == "merged"))
|
|
.count();
|
|
|
|
assert_eq!(merged_count, 1);
|
|
assert_eq!(state_merged_count, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_null_label_fallback() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
insert_label_event(&conn, project_id, Some(issue_id), None, "add", None, 2000);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
|
|
let label_event = events.iter().find(|e| {
|
|
matches!(&e.event_type, TimelineEventType::LabelAdded { label } if label == "[deleted label]")
|
|
});
|
|
assert!(label_event.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_null_milestone_fallback() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
insert_milestone_event(&conn, project_id, Some(issue_id), None, "add", None, 2000);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
|
|
let ms_event = events.iter().find(|e| {
|
|
matches!(&e.event_type, TimelineEventType::MilestoneSet { milestone } if milestone == "[deleted milestone]")
|
|
});
|
|
assert!(ms_event.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_since_filter() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
insert_state_event(&conn, project_id, Some(issue_id), None, "closed", 3000);
|
|
insert_state_event(&conn, project_id, Some(issue_id), None, "reopened", 5000);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
|
|
// Since 4000: should exclude Created (1000) and closed (3000)
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], Some(4000), 100).unwrap();
|
|
assert_eq!(events.len(), 1);
|
|
assert_eq!(events[0].timestamp, 5000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_chronological_sort() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10, Some(4000));
|
|
|
|
insert_state_event(&conn, project_id, Some(issue_id), None, "closed", 3000);
|
|
insert_label_event(
|
|
&conn,
|
|
project_id,
|
|
None,
|
|
Some(mr_id),
|
|
"add",
|
|
Some("bug"),
|
|
2000,
|
|
);
|
|
|
|
let seeds = vec![
|
|
make_entity_ref("issue", issue_id, 1),
|
|
make_entity_ref("merge_request", mr_id, 10),
|
|
];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
|
|
// Verify chronological order
|
|
for window in events.windows(2) {
|
|
assert!(window[0].timestamp <= window[1].timestamp);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_respects_limit() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
for i in 0..20 {
|
|
insert_state_event(
|
|
&conn,
|
|
project_id,
|
|
Some(issue_id),
|
|
None,
|
|
"closed",
|
|
3000 + i * 100,
|
|
);
|
|
}
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let (events, total) = collect_events(&conn, &seeds, &[], &[], &[], None, 5).unwrap();
|
|
assert_eq!(events.len(), 5);
|
|
// 20 state changes + 1 created = 21 total before limit
|
|
assert_eq!(total, 21);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_evidence_notes_included() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
let evidence = vec![TimelineEvent {
|
|
timestamp: 2500,
|
|
entity_type: "issue".to_owned(),
|
|
entity_id: issue_id,
|
|
entity_iid: 1,
|
|
project_path: "group/project".to_owned(),
|
|
event_type: TimelineEventType::NoteEvidence {
|
|
note_id: 42,
|
|
snippet: "relevant note".to_owned(),
|
|
discussion_id: Some(1),
|
|
},
|
|
summary: "Note by alice".to_owned(),
|
|
actor: Some("alice".to_owned()),
|
|
url: None,
|
|
is_seed: true,
|
|
}];
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &evidence, &[], None, 100).unwrap();
|
|
|
|
let note_event = events.iter().find(|e| {
|
|
matches!(
|
|
&e.event_type,
|
|
TimelineEventType::NoteEvidence { note_id, .. } if *note_id == 42
|
|
)
|
|
});
|
|
assert!(note_event.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_merged_fallback_to_state_event() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
// MR with merged_at = NULL
|
|
let mr_id = insert_mr(&conn, project_id, 10, None);
|
|
|
|
// But has a state event for 'merged'
|
|
insert_state_event(&conn, project_id, None, Some(mr_id), "merged", 5000);
|
|
|
|
let seeds = vec![make_entity_ref("merge_request", mr_id, 10)];
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &[], None, 100).unwrap();
|
|
|
|
let merged = events
|
|
.iter()
|
|
.find(|e| matches!(e.event_type, TimelineEventType::Merged));
|
|
assert!(merged.is_some());
|
|
assert_eq!(merged.unwrap().timestamp, 5000);
|
|
}
|
|
|
|
// ─── Discussion thread tests ────────────────────────────────────────────────
|
|
|
|
fn insert_discussion(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
issue_id: Option<i64>,
|
|
mr_id: Option<i64>,
|
|
) -> i64 {
|
|
let noteable_type = if issue_id.is_some() {
|
|
"Issue"
|
|
} else {
|
|
"MergeRequest"
|
|
};
|
|
conn.execute(
|
|
"INSERT INTO discussions (gitlab_discussion_id, project_id, issue_id, merge_request_id, noteable_type, last_seen_at) VALUES (?1, ?2, ?3, ?4, ?5, 0)",
|
|
rusqlite::params![format!("disc_{}", rand::random::<u32>()), project_id, issue_id, mr_id, noteable_type],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_note(
|
|
conn: &Connection,
|
|
discussion_id: i64,
|
|
project_id: i64,
|
|
author: &str,
|
|
body: &str,
|
|
is_system: bool,
|
|
created_at: i64,
|
|
) -> i64 {
|
|
let gitlab_id: i64 = rand::random::<u32>().into();
|
|
conn.execute(
|
|
"INSERT INTO notes (gitlab_id, discussion_id, project_id, is_system, author_username, body, created_at, updated_at, last_seen_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7, ?7)",
|
|
rusqlite::params![gitlab_id, discussion_id, project_id, is_system as i32, author, body, created_at],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
fn make_matched_discussion(
|
|
discussion_id: i64,
|
|
entity_type: &str,
|
|
entity_id: i64,
|
|
project_id: i64,
|
|
) -> MatchedDiscussion {
|
|
MatchedDiscussion {
|
|
discussion_id,
|
|
entity_type: entity_type.to_owned(),
|
|
entity_id,
|
|
project_id,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_basic() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"alice",
|
|
"First note",
|
|
false,
|
|
2000,
|
|
);
|
|
insert_note(&conn, disc_id, project_id, "bob", "Reply here", false, 3000);
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"alice",
|
|
"Follow up",
|
|
false,
|
|
4000,
|
|
);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread = events
|
|
.iter()
|
|
.find(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }));
|
|
assert!(thread.is_some(), "Should have a DiscussionThread event");
|
|
|
|
let thread = thread.unwrap();
|
|
if let TimelineEventType::DiscussionThread {
|
|
discussion_id,
|
|
notes,
|
|
} = &thread.event_type
|
|
{
|
|
assert_eq!(*discussion_id, disc_id);
|
|
assert_eq!(notes.len(), 3);
|
|
assert_eq!(notes[0].author.as_deref(), Some("alice"));
|
|
assert_eq!(notes[0].body, "First note");
|
|
assert_eq!(notes[1].author.as_deref(), Some("bob"));
|
|
assert_eq!(notes[2].body, "Follow up");
|
|
} else {
|
|
panic!("Expected DiscussionThread variant");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_skips_system_notes() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"alice",
|
|
"User note",
|
|
false,
|
|
2000,
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"system",
|
|
"added label ~bug",
|
|
true,
|
|
3000,
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"bob",
|
|
"Another user note",
|
|
false,
|
|
4000,
|
|
);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread = events
|
|
.iter()
|
|
.find(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }));
|
|
assert!(thread.is_some());
|
|
|
|
if let TimelineEventType::DiscussionThread { notes, .. } = &thread.unwrap().event_type {
|
|
assert_eq!(notes.len(), 2, "System notes should be filtered out");
|
|
assert_eq!(notes[0].body, "User note");
|
|
assert_eq!(notes[1].body, "Another user note");
|
|
} else {
|
|
panic!("Expected DiscussionThread");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_empty_after_system_filter() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
// Only system notes
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"system",
|
|
"added label",
|
|
true,
|
|
2000,
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"system",
|
|
"removed label",
|
|
true,
|
|
3000,
|
|
);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread_count = events
|
|
.iter()
|
|
.filter(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }))
|
|
.count();
|
|
assert_eq!(
|
|
thread_count, 0,
|
|
"All-system-note discussion should produce no thread"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_body_truncation() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
let long_body = "x".repeat(10_000);
|
|
insert_note(&conn, disc_id, project_id, "alice", &long_body, false, 2000);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread = events
|
|
.iter()
|
|
.find(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }))
|
|
.unwrap();
|
|
|
|
if let TimelineEventType::DiscussionThread { notes, .. } = &thread.event_type {
|
|
assert_eq!(
|
|
notes[0].body.chars().count(),
|
|
10_000,
|
|
"Body should preserve full text without truncation"
|
|
);
|
|
} else {
|
|
panic!("Expected DiscussionThread");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_note_cap() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
// Insert 60 notes, exceeding THREAD_MAX_NOTES (50)
|
|
for i in 0..60 {
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"alice",
|
|
&format!("Note {i}"),
|
|
false,
|
|
2000 + i * 100,
|
|
);
|
|
}
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread = events
|
|
.iter()
|
|
.find(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }))
|
|
.unwrap();
|
|
|
|
if let TimelineEventType::DiscussionThread { notes, .. } = &thread.event_type {
|
|
// 50 notes + 1 synthetic summary = 51
|
|
assert_eq!(
|
|
notes.len(),
|
|
crate::timeline::THREAD_MAX_NOTES + 1,
|
|
"Should cap at THREAD_MAX_NOTES + synthetic summary"
|
|
);
|
|
let last = notes.last().unwrap();
|
|
assert!(last.body.contains("more notes not shown"));
|
|
} else {
|
|
panic!("Expected DiscussionThread");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_timestamp_is_first_note() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
insert_note(&conn, disc_id, project_id, "alice", "First", false, 5000);
|
|
insert_note(&conn, disc_id, project_id, "bob", "Second", false, 8000);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread = events
|
|
.iter()
|
|
.find(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
thread.timestamp, 5000,
|
|
"Thread timestamp should be first note's created_at"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_sort_position() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
// Note at t=2000 (between Created at t=1000 and state change at t=3000)
|
|
insert_note(
|
|
&conn,
|
|
disc_id,
|
|
project_id,
|
|
"alice",
|
|
"discussion",
|
|
false,
|
|
2000,
|
|
);
|
|
insert_state_event(&conn, project_id, Some(issue_id), None, "closed", 3000);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
let discussions = [make_matched_discussion(
|
|
disc_id, "issue", issue_id, project_id,
|
|
)];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
// Expected order: Created(1000), DiscussionThread(2000), StateChanged(3000)
|
|
assert!(events.len() >= 3);
|
|
assert!(matches!(events[0].event_type, TimelineEventType::Created));
|
|
assert!(matches!(
|
|
events[1].event_type,
|
|
TimelineEventType::DiscussionThread { .. }
|
|
));
|
|
assert!(matches!(
|
|
events[2].event_type,
|
|
TimelineEventType::StateChanged { .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_discussion_thread_dedup() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let disc_id = insert_discussion(&conn, project_id, Some(issue_id), None);
|
|
|
|
insert_note(&conn, disc_id, project_id, "alice", "hello", false, 2000);
|
|
|
|
let seeds = [make_entity_ref("issue", issue_id, 1)];
|
|
// Same discussion_id twice
|
|
let discussions = [
|
|
make_matched_discussion(disc_id, "issue", issue_id, project_id),
|
|
make_matched_discussion(disc_id, "issue", issue_id, project_id),
|
|
];
|
|
|
|
let (events, _) = collect_events(&conn, &seeds, &[], &[], &discussions, None, 100).unwrap();
|
|
|
|
let thread_count = events
|
|
.iter()
|
|
.filter(|e| matches!(&e.event_type, TimelineEventType::DiscussionThread { .. }))
|
|
.count();
|
|
assert_eq!(
|
|
thread_count, 1,
|
|
"Duplicate discussion_id should produce one thread"
|
|
);
|
|
}
|