Complete implementation of the lore me command with:
- Config: gitlab.username field for identity resolution
- CLI: MeArgs with --issues, --mrs, --activity, --since, --project, --all, --user, --fields
- Identity: username resolution with precedence (CLI > config > error)
- Scope: project scope resolution with fuzzy matching and mutual exclusivity
- Types: AttentionState enum (5 states with sort ordering), dashboard structs
- Queries: open issues, authored MRs, reviewing MRs (all with attention state CTEs)
- Activity: 5-source feed (notes, state/label/milestone events, assignment detection)
- Human renderer: summary header, attention legend, section cards, event badges
- Robot renderer: {ok,data,meta} envelope with --fields minimal preset
- Handler: full wiring with section filtering, error paths, exit codes
- Autocorrect: me command flags registered
21 beads (bd-qpk3 through bd-32aw) implemented by 3-agent swarm.
978 tests pass, clippy clean.
1350 lines
42 KiB
Rust
1350 lines
42 KiB
Rust
use super::*;
|
|
use crate::cli::render;
|
|
use crate::core::time::now_ms;
|
|
|
|
#[test]
|
|
fn truncate_leaves_short_strings_alone() {
|
|
assert_eq!(render::truncate("short", 10), "short");
|
|
}
|
|
|
|
#[test]
|
|
fn truncate_adds_ellipsis_to_long_strings() {
|
|
assert_eq!(
|
|
render::truncate("this is a very long title", 15),
|
|
"this is a ve..."
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn truncate_handles_exact_length() {
|
|
assert_eq!(render::truncate("exactly10!", 10), "exactly10!");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_time_formats_correctly() {
|
|
let now = now_ms();
|
|
|
|
assert_eq!(render::format_relative_time(now - 30_000), "just now");
|
|
assert_eq!(render::format_relative_time(now - 120_000), "2 min ago");
|
|
assert_eq!(render::format_relative_time(now - 7_200_000), "2 hours ago");
|
|
assert_eq!(
|
|
render::format_relative_time(now - 172_800_000),
|
|
"2 days ago"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn format_labels_empty() {
|
|
assert_eq!(render::format_labels(&[], 2), "");
|
|
}
|
|
|
|
#[test]
|
|
fn format_labels_single() {
|
|
assert_eq!(render::format_labels(&["bug".to_string()], 2), "[bug]");
|
|
}
|
|
|
|
#[test]
|
|
fn format_labels_multiple() {
|
|
let labels = vec!["bug".to_string(), "urgent".to_string()];
|
|
assert_eq!(render::format_labels(&labels, 2), "[bug, urgent]");
|
|
}
|
|
|
|
#[test]
|
|
fn format_labels_overflow() {
|
|
let labels = vec![
|
|
"bug".to_string(),
|
|
"urgent".to_string(),
|
|
"wip".to_string(),
|
|
"blocked".to_string(),
|
|
];
|
|
assert_eq!(render::format_labels(&labels, 2), "[bug, urgent +2]");
|
|
}
|
|
|
|
#[test]
|
|
fn format_discussions_empty() {
|
|
assert_eq!(format_discussions(0, 0).text, "");
|
|
}
|
|
|
|
#[test]
|
|
fn format_discussions_no_unresolved() {
|
|
assert_eq!(format_discussions(5, 0).text, "5");
|
|
}
|
|
|
|
#[test]
|
|
fn format_discussions_with_unresolved() {
|
|
let cell = format_discussions(5, 2);
|
|
// Text contains styled ANSI for warning-colored unresolved count
|
|
assert!(cell.text.starts_with("5/"), "got: {}", cell.text);
|
|
assert!(cell.text.contains("2!"), "got: {}", cell.text);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Note query layer tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
use std::path::Path;
|
|
|
|
use crate::core::config::{
|
|
Config, EmbeddingConfig, GitLabConfig, LoggingConfig, ProjectConfig, ScoringConfig,
|
|
StorageConfig, SyncConfig,
|
|
};
|
|
use crate::core::db::{create_connection, run_migrations};
|
|
|
|
fn test_config(default_project: Option<&str>) -> Config {
|
|
Config {
|
|
gitlab: GitLabConfig {
|
|
base_url: "https://gitlab.example.com".to_string(),
|
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
|
token: None,
|
|
username: None,
|
|
},
|
|
projects: vec![ProjectConfig {
|
|
path: "group/project".to_string(),
|
|
}],
|
|
default_project: default_project.map(String::from),
|
|
sync: SyncConfig::default(),
|
|
storage: StorageConfig::default(),
|
|
embedding: EmbeddingConfig::default(),
|
|
logging: LoggingConfig::default(),
|
|
scoring: ScoringConfig::default(),
|
|
}
|
|
}
|
|
|
|
fn default_note_filters() -> NoteListFilters {
|
|
NoteListFilters {
|
|
limit: 50,
|
|
project: None,
|
|
author: None,
|
|
note_type: None,
|
|
include_system: false,
|
|
for_issue_iid: None,
|
|
for_mr_iid: None,
|
|
note_id: None,
|
|
gitlab_note_id: None,
|
|
discussion_id: None,
|
|
since: None,
|
|
until: None,
|
|
path: None,
|
|
contains: None,
|
|
resolution: None,
|
|
sort: "created".to_string(),
|
|
order: "desc".to_string(),
|
|
}
|
|
}
|
|
|
|
fn setup_note_test_db() -> Connection {
|
|
let conn = create_connection(Path::new(":memory:")).unwrap();
|
|
run_migrations(&conn).unwrap();
|
|
conn
|
|
}
|
|
|
|
fn insert_test_project(conn: &Connection, id: i64, path: &str) {
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url)
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
rusqlite::params![
|
|
id,
|
|
id * 100,
|
|
path,
|
|
format!("https://gitlab.example.com/{path}")
|
|
],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn insert_test_issue(conn: &Connection, id: i64, project_id: i64, iid: i64, title: &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', 'author', 1000, 2000, 3000)",
|
|
rusqlite::params![id, id * 10, project_id, iid, title],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn insert_test_mr(conn: &Connection, id: i64, project_id: i64, iid: i64, title: &str) {
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state,
|
|
author_username, source_branch, target_branch,
|
|
created_at, updated_at, last_seen_at)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, 'opened', 'author', 'feat', 'main',
|
|
1000, 2000, 3000)",
|
|
rusqlite::params![id, id * 10, project_id, iid, title],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_test_discussion(
|
|
conn: &Connection,
|
|
id: i64,
|
|
gitlab_disc_id: &str,
|
|
project_id: i64,
|
|
issue_id: Option<i64>,
|
|
mr_id: Option<i64>,
|
|
noteable_type: &str,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id,
|
|
merge_request_id, noteable_type, last_seen_at)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1000)",
|
|
rusqlite::params![
|
|
id,
|
|
gitlab_disc_id,
|
|
project_id,
|
|
issue_id,
|
|
mr_id,
|
|
noteable_type
|
|
],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_test_note(
|
|
conn: &Connection,
|
|
id: i64,
|
|
gitlab_id: i64,
|
|
discussion_id: i64,
|
|
project_id: i64,
|
|
author: &str,
|
|
body: &str,
|
|
is_system: bool,
|
|
created_at: i64,
|
|
updated_at: i64,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
|
|
body, is_system, created_at, updated_at, last_seen_at)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
|
rusqlite::params![
|
|
id,
|
|
gitlab_id,
|
|
discussion_id,
|
|
project_id,
|
|
author,
|
|
body,
|
|
is_system as i64,
|
|
created_at,
|
|
updated_at,
|
|
updated_at,
|
|
],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_note_with_position(
|
|
conn: &Connection,
|
|
id: i64,
|
|
gitlab_id: i64,
|
|
discussion_id: i64,
|
|
project_id: i64,
|
|
author: &str,
|
|
body: &str,
|
|
created_at: i64,
|
|
note_type: Option<&str>,
|
|
new_path: Option<&str>,
|
|
new_line: Option<i64>,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
|
|
body, is_system, created_at, updated_at, last_seen_at,
|
|
note_type, position_new_path, position_new_line)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?7, ?8, ?9, ?10)",
|
|
rusqlite::params![
|
|
id,
|
|
gitlab_id,
|
|
discussion_id,
|
|
project_id,
|
|
author,
|
|
body,
|
|
created_at,
|
|
note_type,
|
|
new_path,
|
|
new_line,
|
|
],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_resolvable_note(
|
|
conn: &Connection,
|
|
id: i64,
|
|
gitlab_id: i64,
|
|
discussion_id: i64,
|
|
project_id: i64,
|
|
author: &str,
|
|
body: &str,
|
|
created_at: i64,
|
|
resolvable: bool,
|
|
resolved: bool,
|
|
resolved_by: Option<&str>,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
|
|
body, is_system, created_at, updated_at, last_seen_at,
|
|
resolvable, resolved, resolved_by)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?7, ?8, ?9, ?10)",
|
|
rusqlite::params![
|
|
id,
|
|
gitlab_id,
|
|
discussion_id,
|
|
project_id,
|
|
author,
|
|
body,
|
|
created_at,
|
|
resolvable as i64,
|
|
resolved as i64,
|
|
resolved_by,
|
|
],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_empty_db() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
let filters = default_note_filters();
|
|
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 0);
|
|
assert!(result.notes.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_basic() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 42, "Test Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Hello world",
|
|
false,
|
|
1000,
|
|
2000,
|
|
);
|
|
|
|
let filters = default_note_filters();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes.len(), 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
assert_eq!(result.notes[0].body.as_deref(), Some("Hello world"));
|
|
assert_eq!(result.notes[0].parent_iid, Some(42));
|
|
assert_eq!(result.notes[0].parent_title.as_deref(), Some("Test Issue"));
|
|
assert_eq!(result.notes[0].project_path, "group/project");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_excludes_system_by_default() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "User note", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bot", "System note", true, 2000, 2000);
|
|
|
|
let filters = default_note_filters();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_include_system() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "User note", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bot", "System note", true, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.include_system = true;
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_author() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Note 1", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Note 2", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.author = Some("alice".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_author_case_insensitive() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "Alice", "Note", false, 1000, 1000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.author = Some("ALICE".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_author_strips_at() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Note", false, 1000, 1000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.author = Some("@alice".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_since() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
let ts = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old note", false, ts, ts);
|
|
let ts2 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "New note", false, ts2, ts2);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.since = Some("2024-03-01".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "bob");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_until() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
let ts = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old note", false, ts, ts);
|
|
let ts2 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "New note", false, ts2, ts2);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.until = Some("2024-03-01".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_since_until_combined() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
let ts1 = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old", false, ts1, ts1);
|
|
let ts2 = crate::core::time::iso_to_ms("2024-03-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Middle", false, ts2, ts2);
|
|
let ts3 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
|
|
insert_test_note(&conn, 3, 102, 1, 1, "carol", "New", false, ts3, ts3);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.since = Some("2024-02-01".to_string());
|
|
filters.until = Some("2024-04-01".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "bob");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_invalid_time_window_rejected() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.since = Some("2024-06-01".to_string());
|
|
filters.until = Some("2024-01-01".to_string());
|
|
let result = query_notes(&conn, &filters, &config);
|
|
assert!(result.is_err());
|
|
let msg = result.unwrap_err().to_string();
|
|
assert!(
|
|
msg.contains("--since is after --until"),
|
|
"Expected time window error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_until_date_uses_end_of_day() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
let ts = crate::core::time::iso_to_ms("2024-03-01T23:30:00Z").unwrap();
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Late night", false, ts, ts);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.until = Some("2024-03-01".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(
|
|
result.total_count, 1,
|
|
"Note at 23:30 should be included when --until is the same date"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_contains() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"This has a BUG in it",
|
|
false,
|
|
1000,
|
|
1000,
|
|
);
|
|
insert_test_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"Everything is fine",
|
|
false,
|
|
2000,
|
|
2000,
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.contains = Some("bug".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_contains_escapes_like_wildcards() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "100% done", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "100 things", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.contains = Some("100%".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_path_exact() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_note_with_position(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Change here",
|
|
1000,
|
|
Some("DiffNote"),
|
|
Some("src/main.rs"),
|
|
Some(42),
|
|
);
|
|
insert_note_with_position(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"And here",
|
|
2000,
|
|
Some("DiffNote"),
|
|
Some("src/lib.rs"),
|
|
Some(10),
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.path = Some("src/main.rs".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_path_prefix() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_note_with_position(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"In src",
|
|
1000,
|
|
Some("DiffNote"),
|
|
Some("src/main.rs"),
|
|
Some(42),
|
|
);
|
|
insert_note_with_position(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"Also in src",
|
|
2000,
|
|
Some("DiffNote"),
|
|
Some("src/lib.rs"),
|
|
Some(10),
|
|
);
|
|
insert_note_with_position(
|
|
&conn,
|
|
3,
|
|
102,
|
|
1,
|
|
1,
|
|
"carol",
|
|
"In tests",
|
|
3000,
|
|
Some("DiffNote"),
|
|
Some("tests/test.rs"),
|
|
Some(1),
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.path = Some("src/".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_for_issue_requires_project() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.for_issue_iid = Some(42);
|
|
let result = query_notes(&conn, &filters, &config);
|
|
assert!(result.is_err());
|
|
let msg = result.unwrap_err().to_string();
|
|
assert!(
|
|
msg.contains("project context"),
|
|
"Expected project context error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_for_mr_requires_project() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.for_mr_iid = Some(10);
|
|
let result = query_notes(&conn, &filters, &config);
|
|
assert!(result.is_err());
|
|
let msg = result.unwrap_err().to_string();
|
|
assert!(
|
|
msg.contains("project context"),
|
|
"Expected project context error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_for_issue_with_project() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 42, "Target Issue");
|
|
insert_test_issue(&conn, 2, 1, 43, "Other Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_discussion(&conn, 2, "disc-2", 1, Some(2), None, "Issue");
|
|
insert_test_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"On issue 42",
|
|
false,
|
|
1000,
|
|
1000,
|
|
);
|
|
insert_test_note(&conn, 2, 101, 2, 1, "bob", "On issue 43", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.for_issue_iid = Some(42);
|
|
filters.project = Some("group/project".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_for_issue_uses_default_project() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(Some("group/project"));
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 42, "Target Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"On issue 42",
|
|
false,
|
|
1000,
|
|
1000,
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.for_issue_iid = Some(42);
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_for_mr_with_project() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_mr(&conn, 1, 1, 10, "Target MR");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, None, Some(1), "MergeRequest");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "On MR 10", false, 1000, 1000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.for_mr_iid = Some(10);
|
|
filters.project = Some("group/project".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_resolution_unresolved() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_resolvable_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Unresolved",
|
|
1000,
|
|
true,
|
|
false,
|
|
None,
|
|
);
|
|
insert_resolvable_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"Resolved",
|
|
2000,
|
|
true,
|
|
true,
|
|
Some("carol"),
|
|
);
|
|
insert_test_note(
|
|
&conn,
|
|
3,
|
|
102,
|
|
1,
|
|
1,
|
|
"dave",
|
|
"Not resolvable",
|
|
false,
|
|
3000,
|
|
3000,
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.resolution = Some("unresolved".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_resolution_resolved() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_resolvable_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Unresolved",
|
|
1000,
|
|
true,
|
|
false,
|
|
None,
|
|
);
|
|
insert_resolvable_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"Resolved",
|
|
2000,
|
|
true,
|
|
true,
|
|
Some("carol"),
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.resolution = Some("resolved".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "bob");
|
|
assert!(result.notes[0].resolved);
|
|
assert_eq!(result.notes[0].resolved_by.as_deref(), Some("carol"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_sort_created_desc() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
|
|
insert_test_note(&conn, 3, 102, 1, 1, "carol", "Third", false, 3000, 3000);
|
|
|
|
let filters = default_note_filters();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.notes[0].author_username, "carol");
|
|
assert_eq!(result.notes[1].author_username, "bob");
|
|
assert_eq!(result.notes[2].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_sort_created_asc() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
|
|
insert_test_note(&conn, 3, 102, 1, 1, "carol", "Third", false, 3000, 3000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.order = "asc".to_string();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
assert_eq!(result.notes[1].author_username, "bob");
|
|
assert_eq!(result.notes[2].author_username, "carol");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_deterministic_tiebreak() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "A", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "B", false, 1000, 1000);
|
|
insert_test_note(&conn, 3, 102, 1, 1, "carol", "C", false, 1000, 1000);
|
|
|
|
let filters = default_note_filters();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.notes[0].id, 3);
|
|
assert_eq!(result.notes[1].id, 2);
|
|
assert_eq!(result.notes[2].id, 1);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.order = "asc".to_string();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.notes[0].id, 1);
|
|
assert_eq!(result.notes[1].id, 2);
|
|
assert_eq!(result.notes[2].id, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_limit() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
for i in 1..=10 {
|
|
insert_test_note(
|
|
&conn,
|
|
i,
|
|
100 + i,
|
|
1,
|
|
1,
|
|
"alice",
|
|
&format!("Note {i}"),
|
|
false,
|
|
i * 1000,
|
|
i * 1000,
|
|
);
|
|
}
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.limit = 3;
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 10);
|
|
assert_eq!(result.notes.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_combined_filters() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Found a bug here",
|
|
false,
|
|
1000,
|
|
1000,
|
|
);
|
|
insert_test_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Looks good",
|
|
false,
|
|
2000,
|
|
2000,
|
|
);
|
|
insert_test_note(
|
|
&conn,
|
|
3,
|
|
102,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"Another bug fix",
|
|
false,
|
|
3000,
|
|
3000,
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.author = Some("alice".to_string());
|
|
filters.contains = Some("bug".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].id, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_note_type() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_note_with_position(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"Diff comment",
|
|
1000,
|
|
Some("DiffNote"),
|
|
Some("src/main.rs"),
|
|
Some(10),
|
|
);
|
|
insert_test_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
1,
|
|
"bob",
|
|
"Discussion note",
|
|
false,
|
|
2000,
|
|
2000,
|
|
);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.note_type = Some("DiffNote".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_discussion_id() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-aaa", 1, Some(1), None, "Issue");
|
|
insert_test_discussion(&conn, 2, "disc-bbb", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "In disc A", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 2, 1, "bob", "In disc B", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.discussion_id = Some("disc-aaa".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_note_id() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.note_id = Some(2);
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "bob");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_gitlab_note_id() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 200, 1, 1, "bob", "Second", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.gitlab_note_id = Some(200);
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "bob");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_filter_project() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project-a");
|
|
insert_test_project(&conn, 2, "group/project-b");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue A");
|
|
insert_test_issue(&conn, 2, 2, 1, "Issue B");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_discussion(&conn, 2, "disc-2", 2, Some(2), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "In A", false, 1000, 1000);
|
|
insert_test_note(&conn, 2, 101, 2, 2, "bob", "In B", false, 2000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.project = Some("group/project-a".to_string());
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_mr_parent() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_mr(&conn, 1, 1, 99, "My MR");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, None, Some(1), "MergeRequest");
|
|
insert_test_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
1,
|
|
"alice",
|
|
"MR comment",
|
|
false,
|
|
1000,
|
|
1000,
|
|
);
|
|
|
|
let filters = default_note_filters();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.total_count, 1);
|
|
assert_eq!(
|
|
result.notes[0].noteable_type.as_deref(),
|
|
Some("MergeRequest")
|
|
);
|
|
assert_eq!(result.notes[0].parent_iid, Some(99));
|
|
assert_eq!(result.notes[0].parent_title.as_deref(), Some("My MR"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_list_row_json_conversion() {
|
|
let row = NoteListRow {
|
|
id: 1,
|
|
gitlab_id: 100,
|
|
author_username: "alice".to_string(),
|
|
body: Some("Test body".to_string()),
|
|
note_type: Some("DiffNote".to_string()),
|
|
is_system: false,
|
|
created_at: 1_705_315_800_000,
|
|
updated_at: 1_705_315_800_000,
|
|
position_new_path: Some("src/main.rs".to_string()),
|
|
position_new_line: Some(42),
|
|
position_old_path: None,
|
|
position_old_line: None,
|
|
resolvable: true,
|
|
resolved: false,
|
|
resolved_by: None,
|
|
noteable_type: Some("Issue".to_string()),
|
|
parent_iid: Some(5),
|
|
parent_title: Some("Test Issue".to_string()),
|
|
project_path: "group/project".to_string(),
|
|
};
|
|
|
|
let json_row = NoteListRowJson::from(&row);
|
|
assert_eq!(json_row.id, 1);
|
|
assert_eq!(json_row.gitlab_id, 100);
|
|
assert_eq!(json_row.author_username, "alice");
|
|
assert!(json_row.created_at_iso.contains("2024-01-15"));
|
|
assert!(json_row.updated_at_iso.contains("2024-01-15"));
|
|
assert_eq!(json_row.position_new_path.as_deref(), Some("src/main.rs"));
|
|
assert_eq!(json_row.position_new_line, Some(42));
|
|
assert!(!json_row.is_system);
|
|
assert!(json_row.resolvable);
|
|
assert!(!json_row.resolved);
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_list_result_json_conversion() {
|
|
let result = NoteListResult {
|
|
notes: vec![NoteListRow {
|
|
id: 1,
|
|
gitlab_id: 100,
|
|
author_username: "alice".to_string(),
|
|
body: Some("Test".to_string()),
|
|
note_type: None,
|
|
is_system: false,
|
|
created_at: 1000,
|
|
updated_at: 2000,
|
|
position_new_path: None,
|
|
position_new_line: None,
|
|
position_old_path: None,
|
|
position_old_line: None,
|
|
resolvable: false,
|
|
resolved: false,
|
|
resolved_by: None,
|
|
noteable_type: Some("Issue".to_string()),
|
|
parent_iid: Some(1),
|
|
parent_title: Some("Issue".to_string()),
|
|
project_path: "group/project".to_string(),
|
|
}],
|
|
total_count: 5,
|
|
};
|
|
|
|
let json_result = NoteListResultJson::from(&result);
|
|
assert_eq!(json_result.total_count, 5);
|
|
assert_eq!(json_result.showing, 1);
|
|
assert_eq!(json_result.notes.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_notes_sort_updated() {
|
|
let conn = setup_note_test_db();
|
|
let config = test_config(None);
|
|
insert_test_project(&conn, 1, "group/project");
|
|
insert_test_issue(&conn, 1, 1, 1, "Issue");
|
|
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
|
|
insert_test_note(&conn, 1, 100, 1, 1, "alice", "A", false, 1000, 3000);
|
|
insert_test_note(&conn, 2, 101, 1, 1, "bob", "B", false, 2000, 1000);
|
|
insert_test_note(&conn, 3, 102, 1, 1, "carol", "C", false, 3000, 2000);
|
|
|
|
let mut filters = default_note_filters();
|
|
filters.sort = "updated".to_string();
|
|
filters.order = "desc".to_string();
|
|
let result = query_notes(&conn, &filters, &config).unwrap();
|
|
assert_eq!(result.notes[0].author_username, "alice");
|
|
assert_eq!(result.notes[1].author_username, "carol");
|
|
assert_eq!(result.notes[2].author_username, "bob");
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_escape_like() {
|
|
assert_eq!(note_escape_like("normal/path"), "normal/path");
|
|
assert_eq!(note_escape_like("has_underscore"), "has\\_underscore");
|
|
assert_eq!(note_escape_like("has%percent"), "has\\%percent");
|
|
assert_eq!(note_escape_like("has\\backslash"), "has\\\\backslash");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Note output formatting tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_truncate_note_body() {
|
|
let short = "short body";
|
|
assert_eq!(truncate_body(short, 60), "short body");
|
|
|
|
let long: String = "a".repeat(200);
|
|
let result = truncate_body(&long, 60);
|
|
assert_eq!(result.chars().count(), 63); // 60 chars + "..."
|
|
assert!(result.ends_with("..."));
|
|
}
|
|
|
|
#[test]
|
|
fn test_jsonl_output_one_per_line() {
|
|
let result = NoteListResult {
|
|
notes: vec![
|
|
NoteListRow {
|
|
id: 1,
|
|
gitlab_id: 100,
|
|
author_username: "alice".to_string(),
|
|
body: Some("First note".to_string()),
|
|
note_type: None,
|
|
is_system: false,
|
|
created_at: 1_000_000,
|
|
updated_at: 2_000_000,
|
|
position_new_path: None,
|
|
position_new_line: None,
|
|
position_old_path: None,
|
|
position_old_line: None,
|
|
resolvable: false,
|
|
resolved: false,
|
|
resolved_by: None,
|
|
noteable_type: None,
|
|
parent_iid: None,
|
|
parent_title: None,
|
|
project_path: "group/project".to_string(),
|
|
},
|
|
NoteListRow {
|
|
id: 2,
|
|
gitlab_id: 101,
|
|
author_username: "bob".to_string(),
|
|
body: Some("Second note".to_string()),
|
|
note_type: Some("DiffNote".to_string()),
|
|
is_system: false,
|
|
created_at: 3_000_000,
|
|
updated_at: 4_000_000,
|
|
position_new_path: None,
|
|
position_new_line: None,
|
|
position_old_path: None,
|
|
position_old_line: None,
|
|
resolvable: false,
|
|
resolved: false,
|
|
resolved_by: None,
|
|
noteable_type: None,
|
|
parent_iid: None,
|
|
parent_title: None,
|
|
project_path: "group/project".to_string(),
|
|
},
|
|
],
|
|
total_count: 2,
|
|
};
|
|
|
|
// Each note should produce valid JSON when serialized individually
|
|
for note in &result.notes {
|
|
let json_row = NoteListRowJson::from(note);
|
|
let json_str = serde_json::to_string(&json_row).unwrap();
|
|
// Verify it parses back as valid JSON
|
|
let _: serde_json::Value = serde_json::from_str(&json_str).unwrap();
|
|
// Verify no embedded newlines in the JSON line
|
|
assert!(!json_str.contains('\n'));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_note_parent_variants() {
|
|
assert_eq!(format_note_parent(Some("Issue"), Some(42)), "Issue #42");
|
|
assert_eq!(format_note_parent(Some("MergeRequest"), Some(99)), "MR !99");
|
|
assert_eq!(format_note_parent(None, None), "-");
|
|
assert_eq!(format_note_parent(Some("Issue"), None), "-");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_note_type_variants() {
|
|
assert_eq!(format_note_type(Some("DiffNote")), "Diff");
|
|
assert_eq!(format_note_type(Some("DiscussionNote")), "Disc");
|
|
assert_eq!(format_note_type(None), "-");
|
|
assert_eq!(format_note_type(Some("Other")), "-");
|
|
}
|