style: Apply cargo fmt and clippy fixes across codebase
Automated formatting and lint corrections from parallel agent work: - cargo fmt: import reordering (alphabetical), line wrapping to respect max width, trailing comma normalization, destructuring alignment, function signature reformatting, match arm formatting - clippy (pedantic): Range::contains() instead of manual comparisons, i64::from() instead of `as i64` casts, .clamp() instead of .max().min() chains, let-chain refactors (if-let with &&), #[allow(clippy::too_many_arguments)] and #[allow(clippy::field_reassign_with_default)] where warranted - Removed trailing blank lines and extra whitespace No behavioral changes. All existing tests pass unmodified. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::core::error::Result;
|
||||
use super::truncation::{
|
||||
truncate_discussion, truncate_hard_cap, NoteContent, MAX_DISCUSSION_BYTES,
|
||||
MAX_DISCUSSION_BYTES, NoteContent, truncate_discussion, truncate_hard_cap,
|
||||
};
|
||||
use crate::core::error::Result;
|
||||
|
||||
/// Source type for documents.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -98,22 +98,34 @@ pub fn extract_issue_document(conn: &Connection, issue_id: i64) -> Result<Option
|
||||
rusqlite::params![issue_id],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?, // id
|
||||
row.get::<_, i64>(1)?, // iid
|
||||
row.get::<_, i64>(0)?, // id
|
||||
row.get::<_, i64>(1)?, // iid
|
||||
row.get::<_, Option<String>>(2)?, // title
|
||||
row.get::<_, Option<String>>(3)?, // description
|
||||
row.get::<_, String>(4)?, // state
|
||||
row.get::<_, String>(4)?, // state
|
||||
row.get::<_, Option<String>>(5)?, // author_username
|
||||
row.get::<_, i64>(6)?, // created_at
|
||||
row.get::<_, i64>(7)?, // updated_at
|
||||
row.get::<_, i64>(6)?, // created_at
|
||||
row.get::<_, i64>(7)?, // updated_at
|
||||
row.get::<_, Option<String>>(8)?, // web_url
|
||||
row.get::<_, String>(9)?, // path_with_namespace
|
||||
row.get::<_, i64>(10)?, // project_id
|
||||
row.get::<_, String>(9)?, // path_with_namespace
|
||||
row.get::<_, i64>(10)?, // project_id
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
let (id, iid, title, description, state, author_username, created_at, updated_at, web_url, path_with_namespace, project_id) = match row {
|
||||
let (
|
||||
id,
|
||||
iid,
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
author_username,
|
||||
created_at,
|
||||
updated_at,
|
||||
web_url,
|
||||
path_with_namespace,
|
||||
project_id,
|
||||
) = match row {
|
||||
Ok(r) => r,
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||
Err(e) => return Err(e.into()),
|
||||
@@ -124,15 +136,14 @@ pub fn extract_issue_document(conn: &Connection, issue_id: i64) -> Result<Option
|
||||
"SELECT l.name FROM issue_labels il
|
||||
JOIN labels l ON l.id = il.label_id
|
||||
WHERE il.issue_id = ?1
|
||||
ORDER BY l.name"
|
||||
ORDER BY l.name",
|
||||
)?;
|
||||
let labels: Vec<String> = label_stmt
|
||||
.query_map(rusqlite::params![id], |row| row.get(0))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// Build labels JSON array string
|
||||
let labels_json = serde_json::to_string(&labels)
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let labels_json = serde_json::to_string(&labels).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
// Format content_text per PRD template
|
||||
let display_title = title.as_deref().unwrap_or("(untitled)");
|
||||
@@ -196,24 +207,38 @@ pub fn extract_mr_document(conn: &Connection, mr_id: i64) -> Result<Option<Docum
|
||||
rusqlite::params![mr_id],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?, // id
|
||||
row.get::<_, i64>(1)?, // iid
|
||||
row.get::<_, Option<String>>(2)?, // title
|
||||
row.get::<_, Option<String>>(3)?, // description
|
||||
row.get::<_, Option<String>>(4)?, // state
|
||||
row.get::<_, Option<String>>(5)?, // author_username
|
||||
row.get::<_, Option<String>>(6)?, // source_branch
|
||||
row.get::<_, Option<String>>(7)?, // target_branch
|
||||
row.get::<_, Option<i64>>(8)?, // created_at (nullable in schema)
|
||||
row.get::<_, Option<i64>>(9)?, // updated_at (nullable in schema)
|
||||
row.get::<_, i64>(0)?, // id
|
||||
row.get::<_, i64>(1)?, // iid
|
||||
row.get::<_, Option<String>>(2)?, // title
|
||||
row.get::<_, Option<String>>(3)?, // description
|
||||
row.get::<_, Option<String>>(4)?, // state
|
||||
row.get::<_, Option<String>>(5)?, // author_username
|
||||
row.get::<_, Option<String>>(6)?, // source_branch
|
||||
row.get::<_, Option<String>>(7)?, // target_branch
|
||||
row.get::<_, Option<i64>>(8)?, // created_at (nullable in schema)
|
||||
row.get::<_, Option<i64>>(9)?, // updated_at (nullable in schema)
|
||||
row.get::<_, Option<String>>(10)?, // web_url
|
||||
row.get::<_, String>(11)?, // path_with_namespace
|
||||
row.get::<_, i64>(12)?, // project_id
|
||||
row.get::<_, String>(11)?, // path_with_namespace
|
||||
row.get::<_, i64>(12)?, // project_id
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
let (id, iid, title, description, state, author_username, source_branch, target_branch, created_at, updated_at, web_url, path_with_namespace, project_id) = match row {
|
||||
let (
|
||||
id,
|
||||
iid,
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
author_username,
|
||||
source_branch,
|
||||
target_branch,
|
||||
created_at,
|
||||
updated_at,
|
||||
web_url,
|
||||
path_with_namespace,
|
||||
project_id,
|
||||
) = match row {
|
||||
Ok(r) => r,
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||
Err(e) => return Err(e.into()),
|
||||
@@ -224,14 +249,13 @@ pub fn extract_mr_document(conn: &Connection, mr_id: i64) -> Result<Option<Docum
|
||||
"SELECT l.name FROM mr_labels ml
|
||||
JOIN labels l ON l.id = ml.label_id
|
||||
WHERE ml.merge_request_id = ?1
|
||||
ORDER BY l.name"
|
||||
ORDER BY l.name",
|
||||
)?;
|
||||
let labels: Vec<String> = label_stmt
|
||||
.query_map(rusqlite::params![id], |row| row.get(0))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
let labels_json = serde_json::to_string(&labels)
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let labels_json = serde_json::to_string(&labels).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
let display_title = title.as_deref().unwrap_or("(untitled)");
|
||||
let display_state = state.as_deref().unwrap_or("unknown");
|
||||
@@ -307,12 +331,12 @@ pub fn extract_discussion_document(
|
||||
rusqlite::params![discussion_id],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?, // id
|
||||
row.get::<_, String>(1)?, // noteable_type
|
||||
row.get::<_, Option<i64>>(2)?, // issue_id
|
||||
row.get::<_, Option<i64>>(3)?, // merge_request_id
|
||||
row.get::<_, String>(4)?, // path_with_namespace
|
||||
row.get::<_, i64>(5)?, // project_id
|
||||
row.get::<_, i64>(0)?, // id
|
||||
row.get::<_, String>(1)?, // noteable_type
|
||||
row.get::<_, Option<i64>>(2)?, // issue_id
|
||||
row.get::<_, Option<i64>>(3)?, // merge_request_id
|
||||
row.get::<_, String>(4)?, // path_with_namespace
|
||||
row.get::<_, i64>(5)?, // project_id
|
||||
))
|
||||
},
|
||||
);
|
||||
@@ -359,13 +383,7 @@ pub fn extract_discussion_document(
|
||||
.query_map(rusqlite::params![parent_id], |row| row.get(0))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
(
|
||||
iid,
|
||||
title,
|
||||
web_url,
|
||||
format!("Issue #{}", iid),
|
||||
labels,
|
||||
)
|
||||
(iid, title, web_url, format!("Issue #{}", iid), labels)
|
||||
}
|
||||
"MergeRequest" => {
|
||||
let parent_id = match merge_request_id {
|
||||
@@ -399,13 +417,7 @@ pub fn extract_discussion_document(
|
||||
.query_map(rusqlite::params![parent_id], |row| row.get(0))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
(
|
||||
iid,
|
||||
title,
|
||||
web_url,
|
||||
format!("MR !{}", iid),
|
||||
labels,
|
||||
)
|
||||
(iid, title, web_url, format!("MR !{}", iid), labels)
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
@@ -449,15 +461,15 @@ pub fn extract_discussion_document(
|
||||
// Extract DiffNote paths (deduplicated, sorted)
|
||||
let mut path_set = BTreeSet::new();
|
||||
for note in ¬es {
|
||||
if let Some(ref p) = note.old_path {
|
||||
if !p.is_empty() {
|
||||
path_set.insert(p.clone());
|
||||
}
|
||||
if let Some(ref p) = note.old_path
|
||||
&& !p.is_empty()
|
||||
{
|
||||
path_set.insert(p.clone());
|
||||
}
|
||||
if let Some(ref p) = note.new_path {
|
||||
if !p.is_empty() {
|
||||
path_set.insert(p.clone());
|
||||
}
|
||||
if let Some(ref p) = note.new_path
|
||||
&& !p.is_empty()
|
||||
{
|
||||
path_set.insert(p.clone());
|
||||
}
|
||||
}
|
||||
let paths: Vec<String> = path_set.into_iter().collect();
|
||||
@@ -620,7 +632,8 @@ mod tests {
|
||||
// Helper to create an in-memory DB with the required tables for extraction tests
|
||||
fn setup_test_db() -> Connection {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
conn.execute_batch("
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE projects (
|
||||
id INTEGER PRIMARY KEY,
|
||||
gitlab_project_id INTEGER UNIQUE NOT NULL,
|
||||
@@ -660,7 +673,9 @@ mod tests {
|
||||
label_id INTEGER NOT NULL REFERENCES labels(id),
|
||||
PRIMARY KEY(issue_id, label_id)
|
||||
);
|
||||
").unwrap();
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Insert a test project
|
||||
conn.execute(
|
||||
@@ -671,7 +686,17 @@ mod tests {
|
||||
conn
|
||||
}
|
||||
|
||||
fn insert_issue(conn: &Connection, id: i64, iid: i64, title: Option<&str>, description: Option<&str>, state: &str, author: Option<&str>, web_url: Option<&str>) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_issue(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
iid: i64,
|
||||
title: Option<&str>,
|
||||
description: Option<&str>,
|
||||
state: &str,
|
||||
author: Option<&str>,
|
||||
web_url: Option<&str>,
|
||||
) {
|
||||
conn.execute(
|
||||
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, description, state, author_username, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, 1000, 2000, 3000, ?8)",
|
||||
rusqlite::params![id, id * 10, iid, title, description, state, author, web_url],
|
||||
@@ -682,20 +707,31 @@ mod tests {
|
||||
conn.execute(
|
||||
"INSERT INTO labels (id, project_id, name) VALUES (?1, 1, ?2)",
|
||||
rusqlite::params![id, name],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn link_issue_label(conn: &Connection, issue_id: i64, label_id: i64) {
|
||||
conn.execute(
|
||||
"INSERT INTO issue_labels (issue_id, label_id) VALUES (?1, ?2)",
|
||||
rusqlite::params![issue_id, label_id],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_document_format() {
|
||||
let conn = setup_test_db();
|
||||
insert_issue(&conn, 1, 234, Some("Authentication redesign"), Some("We need to modernize our authentication system..."), "opened", Some("johndoe"), Some("https://gitlab.example.com/group/project-one/-/issues/234"));
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
234,
|
||||
Some("Authentication redesign"),
|
||||
Some("We need to modernize our authentication system..."),
|
||||
"opened",
|
||||
Some("johndoe"),
|
||||
Some("https://gitlab.example.com/group/project-one/-/issues/234"),
|
||||
);
|
||||
insert_label(&conn, 1, "auth");
|
||||
insert_label(&conn, 2, "bug");
|
||||
link_issue_label(&conn, 1, 1);
|
||||
@@ -706,13 +742,23 @@ mod tests {
|
||||
assert_eq!(doc.source_id, 1);
|
||||
assert_eq!(doc.project_id, 1);
|
||||
assert_eq!(doc.author_username, Some("johndoe".to_string()));
|
||||
assert!(doc.content_text.starts_with("[[Issue]] #234: Authentication redesign\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.starts_with("[[Issue]] #234: Authentication redesign\n")
|
||||
);
|
||||
assert!(doc.content_text.contains("Project: group/project-one\n"));
|
||||
assert!(doc.content_text.contains("URL: https://gitlab.example.com/group/project-one/-/issues/234\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("URL: https://gitlab.example.com/group/project-one/-/issues/234\n")
|
||||
);
|
||||
assert!(doc.content_text.contains("Labels: [\"auth\",\"bug\"]\n"));
|
||||
assert!(doc.content_text.contains("State: opened\n"));
|
||||
assert!(doc.content_text.contains("Author: @johndoe\n"));
|
||||
assert!(doc.content_text.contains("--- Description ---\n\nWe need to modernize our authentication system..."));
|
||||
assert!(
|
||||
doc.content_text.contains(
|
||||
"--- Description ---\n\nWe need to modernize our authentication system..."
|
||||
)
|
||||
);
|
||||
assert!(!doc.is_truncated);
|
||||
assert!(doc.paths.is_empty());
|
||||
}
|
||||
@@ -727,7 +773,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_issue_no_description() {
|
||||
let conn = setup_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Quick fix"), None, "opened", Some("alice"), None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Quick fix"),
|
||||
None,
|
||||
"opened",
|
||||
Some("alice"),
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(!doc.content_text.contains("--- Description ---"));
|
||||
@@ -737,7 +792,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_issue_labels_sorted() {
|
||||
let conn = setup_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("Body"), "opened", Some("bob"), None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("Body"),
|
||||
"opened",
|
||||
Some("bob"),
|
||||
None,
|
||||
);
|
||||
insert_label(&conn, 1, "zeta");
|
||||
insert_label(&conn, 2, "alpha");
|
||||
insert_label(&conn, 3, "middle");
|
||||
@@ -747,13 +811,25 @@ mod tests {
|
||||
|
||||
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
||||
assert_eq!(doc.labels, vec!["alpha", "middle", "zeta"]);
|
||||
assert!(doc.content_text.contains("Labels: [\"alpha\",\"middle\",\"zeta\"]"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("Labels: [\"alpha\",\"middle\",\"zeta\"]")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_no_labels() {
|
||||
let conn = setup_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("Body"), "opened", None, None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("Body"),
|
||||
"opened",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(doc.labels.is_empty());
|
||||
@@ -763,7 +839,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_issue_hash_deterministic() {
|
||||
let conn = setup_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("Body"), "opened", Some("alice"), None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("Body"),
|
||||
"opened",
|
||||
Some("alice"),
|
||||
None,
|
||||
);
|
||||
|
||||
let doc1 = extract_issue_document(&conn, 1).unwrap().unwrap();
|
||||
let doc2 = extract_issue_document(&conn, 1).unwrap().unwrap();
|
||||
@@ -786,7 +871,8 @@ mod tests {
|
||||
|
||||
fn setup_mr_test_db() -> Connection {
|
||||
let conn = setup_test_db();
|
||||
conn.execute_batch("
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE merge_requests (
|
||||
id INTEGER PRIMARY KEY,
|
||||
gitlab_id INTEGER UNIQUE NOT NULL,
|
||||
@@ -821,11 +907,25 @@ mod tests {
|
||||
label_id INTEGER REFERENCES labels(id),
|
||||
PRIMARY KEY(merge_request_id, label_id)
|
||||
);
|
||||
").unwrap();
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
conn
|
||||
}
|
||||
|
||||
fn insert_mr(conn: &Connection, id: i64, iid: i64, title: Option<&str>, description: Option<&str>, state: Option<&str>, author: Option<&str>, source_branch: Option<&str>, target_branch: Option<&str>, web_url: Option<&str>) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_mr(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
iid: i64,
|
||||
title: Option<&str>,
|
||||
description: Option<&str>,
|
||||
state: Option<&str>,
|
||||
author: Option<&str>,
|
||||
source_branch: Option<&str>,
|
||||
target_branch: Option<&str>,
|
||||
web_url: Option<&str>,
|
||||
) {
|
||||
conn.execute(
|
||||
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, description, state, author_username, source_branch, target_branch, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1000, 2000, 3000, ?10)",
|
||||
rusqlite::params![id, id * 10, iid, title, description, state, author, source_branch, target_branch, web_url],
|
||||
@@ -836,13 +936,25 @@ mod tests {
|
||||
conn.execute(
|
||||
"INSERT INTO mr_labels (merge_request_id, label_id) VALUES (?1, ?2)",
|
||||
rusqlite::params![mr_id, label_id],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mr_document_format() {
|
||||
let conn = setup_mr_test_db();
|
||||
insert_mr(&conn, 1, 456, Some("Implement JWT authentication"), Some("This MR implements JWT-based authentication..."), Some("opened"), Some("johndoe"), Some("feature/jwt-auth"), Some("main"), Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"));
|
||||
insert_mr(
|
||||
&conn,
|
||||
1,
|
||||
456,
|
||||
Some("Implement JWT authentication"),
|
||||
Some("This MR implements JWT-based authentication..."),
|
||||
Some("opened"),
|
||||
Some("johndoe"),
|
||||
Some("feature/jwt-auth"),
|
||||
Some("main"),
|
||||
Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
|
||||
);
|
||||
insert_label(&conn, 1, "auth");
|
||||
insert_label(&conn, 2, "feature");
|
||||
link_mr_label(&conn, 1, 1);
|
||||
@@ -851,13 +963,25 @@ mod tests {
|
||||
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
||||
assert_eq!(doc.source_type, SourceType::MergeRequest);
|
||||
assert_eq!(doc.source_id, 1);
|
||||
assert!(doc.content_text.starts_with("[[MergeRequest]] !456: Implement JWT authentication\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.starts_with("[[MergeRequest]] !456: Implement JWT authentication\n")
|
||||
);
|
||||
assert!(doc.content_text.contains("Project: group/project-one\n"));
|
||||
assert!(doc.content_text.contains("Labels: [\"auth\",\"feature\"]\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("Labels: [\"auth\",\"feature\"]\n")
|
||||
);
|
||||
assert!(doc.content_text.contains("State: opened\n"));
|
||||
assert!(doc.content_text.contains("Author: @johndoe\n"));
|
||||
assert!(doc.content_text.contains("Source: feature/jwt-auth -> main\n"));
|
||||
assert!(doc.content_text.contains("--- Description ---\n\nThis MR implements JWT-based authentication..."));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("Source: feature/jwt-auth -> main\n")
|
||||
);
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("--- Description ---\n\nThis MR implements JWT-based authentication...")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -870,26 +994,65 @@ mod tests {
|
||||
#[test]
|
||||
fn test_mr_no_description() {
|
||||
let conn = setup_mr_test_db();
|
||||
insert_mr(&conn, 1, 10, Some("Quick fix"), None, Some("merged"), Some("alice"), Some("fix/bug"), Some("main"), None);
|
||||
insert_mr(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Quick fix"),
|
||||
None,
|
||||
Some("merged"),
|
||||
Some("alice"),
|
||||
Some("fix/bug"),
|
||||
Some("main"),
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(!doc.content_text.contains("--- Description ---"));
|
||||
assert!(doc.content_text.contains("[[MergeRequest]] !10: Quick fix\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("[[MergeRequest]] !10: Quick fix\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mr_branch_info() {
|
||||
let conn = setup_mr_test_db();
|
||||
insert_mr(&conn, 1, 10, Some("Test"), Some("Body"), Some("opened"), None, Some("feature/foo"), Some("develop"), None);
|
||||
insert_mr(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("Body"),
|
||||
Some("opened"),
|
||||
None,
|
||||
Some("feature/foo"),
|
||||
Some("develop"),
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(doc.content_text.contains("Source: feature/foo -> develop\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("Source: feature/foo -> develop\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mr_no_branches() {
|
||||
let conn = setup_mr_test_db();
|
||||
insert_mr(&conn, 1, 10, Some("Test"), None, Some("opened"), None, None, None, None);
|
||||
insert_mr(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
None,
|
||||
Some("opened"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(!doc.content_text.contains("Source:"));
|
||||
@@ -899,7 +1062,8 @@ mod tests {
|
||||
|
||||
fn setup_discussion_test_db() -> Connection {
|
||||
let conn = setup_mr_test_db(); // includes projects, issues schema, labels, mr tables
|
||||
conn.execute_batch("
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE discussions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
gitlab_discussion_id TEXT NOT NULL,
|
||||
@@ -937,18 +1101,38 @@ mod tests {
|
||||
position_new_line INTEGER,
|
||||
raw_payload_id INTEGER
|
||||
);
|
||||
").unwrap();
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
conn
|
||||
}
|
||||
|
||||
fn insert_discussion(conn: &Connection, id: i64, noteable_type: &str, issue_id: Option<i64>, mr_id: Option<i64>) {
|
||||
fn insert_discussion(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
noteable_type: &str,
|
||||
issue_id: Option<i64>,
|
||||
mr_id: Option<i64>,
|
||||
) {
|
||||
conn.execute(
|
||||
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, merge_request_id, noteable_type, last_seen_at) VALUES (?1, ?2, 1, ?3, ?4, ?5, 3000)",
|
||||
rusqlite::params![id, format!("disc_{}", id), issue_id, mr_id, noteable_type],
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
fn insert_note(conn: &Connection, id: i64, gitlab_id: i64, discussion_id: i64, author: Option<&str>, body: Option<&str>, created_at: i64, is_system: bool, old_path: Option<&str>, new_path: Option<&str>) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_note(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
gitlab_id: i64,
|
||||
discussion_id: i64,
|
||||
author: Option<&str>,
|
||||
body: Option<&str>,
|
||||
created_at: i64,
|
||||
is_system: bool,
|
||||
old_path: Option<&str>,
|
||||
new_path: Option<&str>,
|
||||
) {
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position_old_path, position_new_path) VALUES (?1, ?2, ?3, 1, ?4, ?5, ?6, ?6, ?6, ?7, ?8, ?9)",
|
||||
rusqlite::params![id, gitlab_id, discussion_id, author, body, created_at, is_system as i32, old_path, new_path],
|
||||
@@ -958,25 +1142,67 @@ mod tests {
|
||||
#[test]
|
||||
fn test_discussion_document_format() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_issue(&conn, 1, 234, Some("Authentication redesign"), Some("desc"), "opened", Some("johndoe"), Some("https://gitlab.example.com/group/project-one/-/issues/234"));
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
234,
|
||||
Some("Authentication redesign"),
|
||||
Some("desc"),
|
||||
"opened",
|
||||
Some("johndoe"),
|
||||
Some("https://gitlab.example.com/group/project-one/-/issues/234"),
|
||||
);
|
||||
insert_label(&conn, 1, "auth");
|
||||
insert_label(&conn, 2, "bug");
|
||||
link_issue_label(&conn, 1, 1);
|
||||
link_issue_label(&conn, 1, 2);
|
||||
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
||||
// 1710460800000 = 2024-03-15T00:00:00Z
|
||||
insert_note(&conn, 1, 12345, 1, Some("johndoe"), Some("I think we should move to JWT-based auth..."), 1710460800000, false, None, None);
|
||||
insert_note(&conn, 2, 12346, 1, Some("janedoe"), Some("Agreed. What about refresh token strategy?"), 1710460800000, false, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
12345,
|
||||
1,
|
||||
Some("johndoe"),
|
||||
Some("I think we should move to JWT-based auth..."),
|
||||
1710460800000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_note(
|
||||
&conn,
|
||||
2,
|
||||
12346,
|
||||
1,
|
||||
Some("janedoe"),
|
||||
Some("Agreed. What about refresh token strategy?"),
|
||||
1710460800000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
||||
assert_eq!(doc.source_type, SourceType::Discussion);
|
||||
assert!(doc.content_text.starts_with("[[Discussion]] Issue #234: Authentication redesign\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.starts_with("[[Discussion]] Issue #234: Authentication redesign\n")
|
||||
);
|
||||
assert!(doc.content_text.contains("Project: group/project-one\n"));
|
||||
assert!(doc.content_text.contains("URL: https://gitlab.example.com/group/project-one/-/issues/234#note_12345\n"));
|
||||
assert!(doc.content_text.contains(
|
||||
"URL: https://gitlab.example.com/group/project-one/-/issues/234#note_12345\n"
|
||||
));
|
||||
assert!(doc.content_text.contains("Labels: [\"auth\",\"bug\"]\n"));
|
||||
assert!(doc.content_text.contains("--- Thread ---"));
|
||||
assert!(doc.content_text.contains("@johndoe (2024-03-15):\nI think we should move to JWT-based auth..."));
|
||||
assert!(doc.content_text.contains("@janedoe (2024-03-15):\nAgreed. What about refresh token strategy?"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("@johndoe (2024-03-15):\nI think we should move to JWT-based auth...")
|
||||
);
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("@janedoe (2024-03-15):\nAgreed. What about refresh token strategy?")
|
||||
);
|
||||
assert_eq!(doc.author_username, Some("johndoe".to_string()));
|
||||
assert!(doc.title.is_none()); // Discussions don't have their own title
|
||||
}
|
||||
@@ -992,13 +1218,34 @@ mod tests {
|
||||
fn test_discussion_parent_deleted() {
|
||||
let conn = setup_discussion_test_db();
|
||||
// Insert issue, create discussion, then delete the issue
|
||||
insert_issue(&conn, 99, 10, Some("To be deleted"), None, "opened", None, None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
99,
|
||||
10,
|
||||
Some("To be deleted"),
|
||||
None,
|
||||
"opened",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_discussion(&conn, 1, "Issue", Some(99), None);
|
||||
insert_note(&conn, 1, 100, 1, Some("alice"), Some("Hello"), 1000, false, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
Some("alice"),
|
||||
Some("Hello"),
|
||||
1000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
// Delete the parent issue — FK cascade won't delete discussion in test since
|
||||
// we used REFERENCES without ON DELETE CASCADE in test schema, so just delete from issues
|
||||
conn.execute("PRAGMA foreign_keys = OFF", []).unwrap();
|
||||
conn.execute("DELETE FROM issues WHERE id = 99", []).unwrap();
|
||||
conn.execute("DELETE FROM issues WHERE id = 99", [])
|
||||
.unwrap();
|
||||
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
|
||||
|
||||
let result = extract_discussion_document(&conn, 1).unwrap();
|
||||
@@ -1008,11 +1255,53 @@ mod tests {
|
||||
#[test]
|
||||
fn test_discussion_system_notes_excluded() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("desc"), "opened", Some("alice"), None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("desc"),
|
||||
"opened",
|
||||
Some("alice"),
|
||||
None,
|
||||
);
|
||||
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
||||
insert_note(&conn, 1, 100, 1, Some("alice"), Some("Real comment"), 1000, false, None, None);
|
||||
insert_note(&conn, 2, 101, 1, Some("bot"), Some("assigned to @alice"), 2000, true, None, None);
|
||||
insert_note(&conn, 3, 102, 1, Some("bob"), Some("Follow-up"), 3000, false, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
Some("alice"),
|
||||
Some("Real comment"),
|
||||
1000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_note(
|
||||
&conn,
|
||||
2,
|
||||
101,
|
||||
1,
|
||||
Some("bot"),
|
||||
Some("assigned to @alice"),
|
||||
2000,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_note(
|
||||
&conn,
|
||||
3,
|
||||
102,
|
||||
1,
|
||||
Some("bob"),
|
||||
Some("Follow-up"),
|
||||
3000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(doc.content_text.contains("@alice"));
|
||||
@@ -1023,38 +1312,115 @@ mod tests {
|
||||
#[test]
|
||||
fn test_discussion_diffnote_paths() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("desc"), "opened", None, None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("desc"),
|
||||
"opened",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
||||
insert_note(&conn, 1, 100, 1, Some("alice"), Some("Comment on code"), 1000, false, Some("src/old.rs"), Some("src/new.rs"));
|
||||
insert_note(&conn, 2, 101, 1, Some("bob"), Some("Reply"), 2000, false, Some("src/old.rs"), Some("src/new.rs"));
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
Some("alice"),
|
||||
Some("Comment on code"),
|
||||
1000,
|
||||
false,
|
||||
Some("src/old.rs"),
|
||||
Some("src/new.rs"),
|
||||
);
|
||||
insert_note(
|
||||
&conn,
|
||||
2,
|
||||
101,
|
||||
1,
|
||||
Some("bob"),
|
||||
Some("Reply"),
|
||||
2000,
|
||||
false,
|
||||
Some("src/old.rs"),
|
||||
Some("src/new.rs"),
|
||||
);
|
||||
|
||||
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
||||
// Paths should be deduplicated and sorted
|
||||
assert_eq!(doc.paths, vec!["src/new.rs", "src/old.rs"]);
|
||||
assert!(doc.content_text.contains("Files: [\"src/new.rs\",\"src/old.rs\"]"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("Files: [\"src/new.rs\",\"src/old.rs\"]")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discussion_url_construction() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("desc"), "opened", None, Some("https://gitlab.example.com/group/project-one/-/issues/10"));
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("desc"),
|
||||
"opened",
|
||||
None,
|
||||
Some("https://gitlab.example.com/group/project-one/-/issues/10"),
|
||||
);
|
||||
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
||||
insert_note(&conn, 1, 54321, 1, Some("alice"), Some("Hello"), 1000, false, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
54321,
|
||||
1,
|
||||
Some("alice"),
|
||||
Some("Hello"),
|
||||
1000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
||||
assert_eq!(doc.url, Some("https://gitlab.example.com/group/project-one/-/issues/10#note_54321".to_string()));
|
||||
assert_eq!(
|
||||
doc.url,
|
||||
Some("https://gitlab.example.com/group/project-one/-/issues/10#note_54321".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discussion_uses_parent_labels() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("desc"), "opened", None, None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("desc"),
|
||||
"opened",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_label(&conn, 1, "backend");
|
||||
insert_label(&conn, 2, "api");
|
||||
link_issue_label(&conn, 1, 1);
|
||||
link_issue_label(&conn, 1, 2);
|
||||
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
||||
insert_note(&conn, 1, 100, 1, Some("alice"), Some("Comment"), 1000, false, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
Some("alice"),
|
||||
Some("Comment"),
|
||||
1000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
||||
assert_eq!(doc.labels, vec!["api", "backend"]);
|
||||
@@ -1063,20 +1429,65 @@ mod tests {
|
||||
#[test]
|
||||
fn test_discussion_on_mr() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_mr(&conn, 1, 456, Some("JWT Auth"), Some("desc"), Some("opened"), Some("johndoe"), Some("feature/jwt"), Some("main"), Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"));
|
||||
insert_mr(
|
||||
&conn,
|
||||
1,
|
||||
456,
|
||||
Some("JWT Auth"),
|
||||
Some("desc"),
|
||||
Some("opened"),
|
||||
Some("johndoe"),
|
||||
Some("feature/jwt"),
|
||||
Some("main"),
|
||||
Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
|
||||
);
|
||||
insert_discussion(&conn, 1, "MergeRequest", None, Some(1));
|
||||
insert_note(&conn, 1, 100, 1, Some("alice"), Some("LGTM"), 1000, false, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
Some("alice"),
|
||||
Some("LGTM"),
|
||||
1000,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
||||
assert!(doc.content_text.contains("[[Discussion]] MR !456: JWT Auth\n"));
|
||||
assert!(
|
||||
doc.content_text
|
||||
.contains("[[Discussion]] MR !456: JWT Auth\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discussion_all_system_notes() {
|
||||
let conn = setup_discussion_test_db();
|
||||
insert_issue(&conn, 1, 10, Some("Test"), Some("desc"), "opened", None, None);
|
||||
insert_issue(
|
||||
&conn,
|
||||
1,
|
||||
10,
|
||||
Some("Test"),
|
||||
Some("desc"),
|
||||
"opened",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
||||
insert_note(&conn, 1, 100, 1, Some("bot"), Some("assigned to @alice"), 1000, true, None, None);
|
||||
insert_note(
|
||||
&conn,
|
||||
1,
|
||||
100,
|
||||
1,
|
||||
Some("bot"),
|
||||
Some("assigned to @alice"),
|
||||
1000,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// All notes are system notes -> no content -> returns None
|
||||
let result = extract_discussion_document(&conn, 1).unwrap();
|
||||
|
||||
@@ -7,11 +7,11 @@ mod regenerator;
|
||||
mod truncation;
|
||||
|
||||
pub use extractor::{
|
||||
compute_content_hash, compute_list_hash, extract_discussion_document,
|
||||
extract_issue_document, extract_mr_document, DocumentData, SourceType,
|
||||
DocumentData, SourceType, compute_content_hash, compute_list_hash, extract_discussion_document,
|
||||
extract_issue_document, extract_mr_document,
|
||||
};
|
||||
pub use regenerator::{regenerate_dirty_documents, RegenerateResult};
|
||||
pub use regenerator::{RegenerateResult, regenerate_dirty_documents};
|
||||
pub use truncation::{
|
||||
truncate_discussion, truncate_hard_cap, truncate_utf8, NoteContent, TruncationReason,
|
||||
TruncationResult, MAX_DISCUSSION_BYTES, MAX_DOCUMENT_BYTES_HARD,
|
||||
MAX_DISCUSSION_BYTES, MAX_DOCUMENT_BYTES_HARD, NoteContent, TruncationReason, TruncationResult,
|
||||
truncate_discussion, truncate_hard_cap, truncate_utf8,
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ use tracing::{debug, warn};
|
||||
|
||||
use crate::core::error::Result;
|
||||
use crate::documents::{
|
||||
extract_discussion_document, extract_issue_document, extract_mr_document, DocumentData,
|
||||
SourceType,
|
||||
DocumentData, SourceType, extract_discussion_document, extract_issue_document,
|
||||
extract_mr_document,
|
||||
};
|
||||
use crate::ingestion::dirty_tracker::{clear_dirty, get_dirty_sources, record_dirty_error};
|
||||
|
||||
@@ -65,11 +65,7 @@ pub fn regenerate_dirty_documents(conn: &Connection) -> Result<RegenerateResult>
|
||||
}
|
||||
|
||||
/// Regenerate a single document. Returns true if content_hash changed.
|
||||
fn regenerate_one(
|
||||
conn: &Connection,
|
||||
source_type: SourceType,
|
||||
source_id: i64,
|
||||
) -> Result<bool> {
|
||||
fn regenerate_one(conn: &Connection, source_type: SourceType, source_id: i64) -> Result<bool> {
|
||||
let doc = match source_type {
|
||||
SourceType::Issue => extract_issue_document(conn, source_id)?,
|
||||
SourceType::MergeRequest => extract_mr_document(conn, source_id)?,
|
||||
@@ -97,8 +93,8 @@ fn get_existing_hash(
|
||||
source_type: SourceType,
|
||||
source_id: i64,
|
||||
) -> Result<Option<String>> {
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT content_hash FROM documents WHERE source_type = ?1 AND source_id = ?2")?;
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT content_hash FROM documents WHERE source_type = ?1 AND source_id = ?2")?;
|
||||
|
||||
let hash: Option<String> = stmt
|
||||
.query_row(rusqlite::params![source_type.as_str(), source_id], |row| {
|
||||
@@ -140,17 +136,15 @@ fn upsert_document_inner(conn: &Connection, doc: &DocumentData) -> Result<()> {
|
||||
.optional()?;
|
||||
|
||||
// Fast path: skip ALL writes when nothing changed (prevents WAL churn)
|
||||
if let Some((_, ref old_content_hash, ref old_labels_hash, ref old_paths_hash)) = existing {
|
||||
if old_content_hash == &doc.content_hash
|
||||
&& old_labels_hash == &doc.labels_hash
|
||||
&& old_paths_hash == &doc.paths_hash
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if let Some((_, ref old_content_hash, ref old_labels_hash, ref old_paths_hash)) = existing
|
||||
&& old_content_hash == &doc.content_hash
|
||||
&& old_labels_hash == &doc.labels_hash
|
||||
&& old_paths_hash == &doc.paths_hash
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let labels_json =
|
||||
serde_json::to_string(&doc.labels).unwrap_or_else(|_| "[]".to_string());
|
||||
let labels_json = serde_json::to_string(&doc.labels).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
// Upsert document row
|
||||
conn.execute(
|
||||
@@ -237,11 +231,7 @@ fn upsert_document_inner(conn: &Connection, doc: &DocumentData) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Delete a document by source identity.
|
||||
fn delete_document(
|
||||
conn: &Connection,
|
||||
source_type: SourceType,
|
||||
source_id: i64,
|
||||
) -> Result<()> {
|
||||
fn delete_document(conn: &Connection, source_type: SourceType, source_id: i64) -> Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM documents WHERE source_type = ?1 AND source_id = ?2",
|
||||
rusqlite::params![source_type.as_str(), source_id],
|
||||
@@ -250,11 +240,7 @@ fn delete_document(
|
||||
}
|
||||
|
||||
/// Get document ID by source type and source ID.
|
||||
fn get_document_id(
|
||||
conn: &Connection,
|
||||
source_type: SourceType,
|
||||
source_id: i64,
|
||||
) -> Result<i64> {
|
||||
fn get_document_id(conn: &Connection, source_type: SourceType, source_id: i64) -> Result<i64> {
|
||||
let id: i64 = conn.query_row(
|
||||
"SELECT id FROM documents WHERE source_type = ?1 AND source_id = ?2",
|
||||
rusqlite::params![source_type.as_str(), source_id],
|
||||
@@ -372,10 +358,14 @@ mod tests {
|
||||
assert_eq!(result.errored, 0);
|
||||
|
||||
// Verify document was created
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0)).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 1);
|
||||
|
||||
let content: String = conn.query_row("SELECT content_text FROM documents", [], |r| r.get(0)).unwrap();
|
||||
let content: String = conn
|
||||
.query_row("SELECT content_text FROM documents", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert!(content.contains("[[Issue]] #42: Test Issue"));
|
||||
}
|
||||
|
||||
@@ -418,7 +408,9 @@ mod tests {
|
||||
let result = regenerate_dirty_documents(&conn).unwrap();
|
||||
assert_eq!(result.regenerated, 1); // Deletion counts as "changed"
|
||||
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0)).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
@@ -451,11 +443,13 @@ mod tests {
|
||||
conn.execute(
|
||||
"INSERT INTO labels (id, project_id, name) VALUES (1, 1, 'bug')",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO issue_labels (issue_id, label_id) VALUES (1, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// First run creates document
|
||||
mark_dirty(&conn, SourceType::Issue, 1).unwrap();
|
||||
@@ -467,9 +461,9 @@ mod tests {
|
||||
assert_eq!(result.unchanged, 1);
|
||||
|
||||
// Labels should still be present (not deleted and re-inserted)
|
||||
let label_count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM document_labels", [], |r| r.get(0),
|
||||
).unwrap();
|
||||
let label_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM document_labels", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(label_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,10 +231,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_first_last_oversized() {
|
||||
let big_body = "x".repeat(20_000);
|
||||
let notes = vec![
|
||||
make_note("alice", &big_body),
|
||||
make_note("bob", &big_body),
|
||||
];
|
||||
let notes = vec![make_note("alice", &big_body), make_note("bob", &big_body)];
|
||||
let result = truncate_discussion(¬es, 10_000);
|
||||
assert!(result.is_truncated);
|
||||
assert_eq!(result.reason, Some(TruncationReason::FirstLastOversized));
|
||||
@@ -304,7 +301,11 @@ mod tests {
|
||||
.collect();
|
||||
let result = truncate_discussion(¬es, 12_000);
|
||||
assert!(result.is_truncated);
|
||||
assert!(result.content.contains("[... 5 notes omitted for length ...]"));
|
||||
assert!(
|
||||
result
|
||||
.content
|
||||
.contains("[... 5 notes omitted for length ...]")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user