feat(queue): support multi-source processed tickets
This commit is contained in:
parent
0149e4ca97
commit
fc434fe560
8 changed files with 247 additions and 111 deletions
|
|
@ -44,12 +44,7 @@ pub fn retry_ticket(state: State<'_, AppState>, ticket_id: String) -> Result<(),
|
|||
|
||||
if let Some(wt) = Worktree::get_by_ticket_id(&conn, &ticket_id)? {
|
||||
if wt.status == "Active" {
|
||||
let project_id = {
|
||||
let tracker =
|
||||
crate::models::tracker::WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?;
|
||||
tracker.project_id
|
||||
};
|
||||
let project = crate::models::project::Project::get_by_id(&conn, &project_id)?;
|
||||
let project = crate::models::project::Project::get_by_id(&conn, &ticket.project_id)?;
|
||||
let _ = crate::services::worktree_manager::delete_worktree(
|
||||
&project.path,
|
||||
&wt.path,
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ pub async fn manual_poll(
|
|||
|
||||
if let Some(ticket) = ProcessedTicket::insert_if_new(
|
||||
&db,
|
||||
&tracker.project_id,
|
||||
&tracker.id,
|
||||
artifact_id,
|
||||
&artifact_title,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::error::AppError;
|
||||
use crate::models::project::Project;
|
||||
use crate::models::ticket::ProcessedTicket;
|
||||
use crate::models::tracker::WatchedTracker;
|
||||
use crate::models::worktree::Worktree;
|
||||
use crate::services::worktree_manager;
|
||||
use crate::AppState;
|
||||
|
|
@ -26,8 +25,7 @@ pub fn get_worktree_diff(
|
|||
|
||||
let wt = Worktree::get_by_id(&conn, &worktree_id)?;
|
||||
let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?;
|
||||
let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?;
|
||||
let project = Project::get_by_id(&conn, &tracker.project_id)?;
|
||||
let project = Project::get_by_id(&conn, &ticket.project_id)?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
|
|
@ -52,8 +50,7 @@ pub fn apply_fix_to_branch(
|
|||
|
||||
let wt = Worktree::get_by_id(&conn, &worktree_id)?;
|
||||
let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?;
|
||||
let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?;
|
||||
let project = Project::get_by_id(&conn, &tracker.project_id)?;
|
||||
let project = Project::get_by_id(&conn, &ticket.project_id)?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
|
|
@ -89,8 +86,7 @@ pub fn delete_worktree_cmd(
|
|||
|
||||
let wt = Worktree::get_by_id(&conn, &worktree_id)?;
|
||||
let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?;
|
||||
let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?;
|
||||
let project = Project::get_by_id(&conn, &tracker.project_id)?;
|
||||
let project = Project::get_by_id(&conn, &ticket.project_id)?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
|
|
@ -126,8 +122,7 @@ pub fn list_local_branches_for_worktree(
|
|||
|
||||
let wt = Worktree::get_by_id(&conn, &worktree_id)?;
|
||||
let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?;
|
||||
let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?;
|
||||
let project = Project::get_by_id(&conn, &tracker.project_id)?;
|
||||
let project = Project::get_by_id(&conn, &ticket.project_id)?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ use uuid::Uuid;
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessedTicket {
|
||||
pub id: String,
|
||||
pub tracker_id: String,
|
||||
pub tracker_id: Option<String>,
|
||||
pub project_id: String,
|
||||
pub source: String,
|
||||
pub source_ref: Option<String>,
|
||||
pub artifact_id: i32,
|
||||
pub artifact_title: String,
|
||||
pub artifact_data: String,
|
||||
|
|
@ -30,28 +33,32 @@ fn from_row(row: &rusqlite::Row) -> rusqlite::Result<ProcessedTicket> {
|
|||
Ok(ProcessedTicket {
|
||||
id: row.get(0)?,
|
||||
tracker_id: row.get(1)?,
|
||||
artifact_id: row.get(2)?,
|
||||
artifact_title: row.get(3)?,
|
||||
artifact_data: row.get(4)?,
|
||||
status: row.get(5)?,
|
||||
analyst_report: row.get(6)?,
|
||||
developer_report: row.get(7)?,
|
||||
worktree_path: row.get(8)?,
|
||||
branch_name: row.get(9)?,
|
||||
detected_at: row.get(10)?,
|
||||
processed_at: row.get(11)?,
|
||||
project_id: row.get(2)?,
|
||||
source: row.get(3)?,
|
||||
source_ref: row.get(4)?,
|
||||
artifact_id: row.get(5)?,
|
||||
artifact_title: row.get(6)?,
|
||||
artifact_data: row.get(7)?,
|
||||
status: row.get(8)?,
|
||||
analyst_report: row.get(9)?,
|
||||
developer_report: row.get(10)?,
|
||||
worktree_path: row.get(11)?,
|
||||
branch_name: row.get(12)?,
|
||||
detected_at: row.get(13)?,
|
||||
processed_at: row.get(14)?,
|
||||
})
|
||||
}
|
||||
|
||||
const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, artifact_id, artifact_title, artifact_data, \
|
||||
status, analyst_report, developer_report, worktree_path, branch_name, \
|
||||
detected_at, processed_at FROM processed_tickets";
|
||||
const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, project_id, source, source_ref, \
|
||||
artifact_id, artifact_title, artifact_data, status, analyst_report, developer_report, \
|
||||
worktree_path, branch_name, detected_at, processed_at FROM processed_tickets";
|
||||
|
||||
impl ProcessedTicket {
|
||||
/// Atomically insert a new ticket keyed by (tracker_id, artifact_id).
|
||||
/// Returns Some(ticket) if inserted, None if it was a duplicate.
|
||||
pub fn insert_if_new(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
tracker_id: &str,
|
||||
artifact_id: i32,
|
||||
artifact_title: &str,
|
||||
|
|
@ -62,16 +69,9 @@ impl ProcessedTicket {
|
|||
|
||||
let inserted_rows = conn.execute(
|
||||
"INSERT OR IGNORE INTO processed_tickets \
|
||||
(id, tracker_id, project_id, artifact_id, artifact_title, artifact_data, status, detected_at) \
|
||||
VALUES (?1, ?2, (SELECT project_id FROM watched_trackers WHERE id = ?2), ?3, ?4, ?5, 'Pending', ?6)",
|
||||
params![
|
||||
id,
|
||||
tracker_id,
|
||||
artifact_id,
|
||||
artifact_title,
|
||||
artifact_data,
|
||||
now
|
||||
],
|
||||
(id, tracker_id, project_id, source, source_ref, artifact_id, artifact_title, artifact_data, status, detected_at) \
|
||||
VALUES (?1, ?2, ?3, 'tuleap', NULL, ?4, ?5, ?6, 'Pending', ?7)",
|
||||
params![id, tracker_id, project_id, artifact_id, artifact_title, artifact_data, now],
|
||||
)?;
|
||||
|
||||
if inserted_rows == 0 {
|
||||
|
|
@ -80,7 +80,10 @@ impl ProcessedTicket {
|
|||
|
||||
let ticket = ProcessedTicket {
|
||||
id,
|
||||
tracker_id: tracker_id.to_string(),
|
||||
tracker_id: Some(tracker_id.to_string()),
|
||||
project_id: project_id.to_string(),
|
||||
source: "tuleap".to_string(),
|
||||
source_ref: None,
|
||||
artifact_id,
|
||||
artifact_title: artifact_title.to_string(),
|
||||
artifact_data: artifact_data.to_string(),
|
||||
|
|
@ -96,6 +99,53 @@ impl ProcessedTicket {
|
|||
Ok(Some(ticket))
|
||||
}
|
||||
|
||||
pub fn insert_external(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
source: &str,
|
||||
source_ref: Option<&str>,
|
||||
artifact_id: i32,
|
||||
artifact_title: &str,
|
||||
artifact_data: &str,
|
||||
) -> Result<ProcessedTicket> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO processed_tickets \
|
||||
(id, tracker_id, project_id, source, source_ref, artifact_id, artifact_title, artifact_data, status, detected_at) \
|
||||
VALUES (?1, NULL, ?2, ?3, ?4, ?5, ?6, ?7, 'Pending', ?8)",
|
||||
params![
|
||||
id,
|
||||
project_id,
|
||||
source,
|
||||
source_ref,
|
||||
artifact_id,
|
||||
artifact_title,
|
||||
artifact_data,
|
||||
now
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(ProcessedTicket {
|
||||
id,
|
||||
tracker_id: None,
|
||||
project_id: project_id.to_string(),
|
||||
source: source.to_string(),
|
||||
source_ref: source_ref.map(|value| value.to_string()),
|
||||
artifact_id,
|
||||
artifact_title: artifact_title.to_string(),
|
||||
artifact_data: artifact_data.to_string(),
|
||||
status: "Pending".to_string(),
|
||||
analyst_report: None,
|
||||
developer_report: None,
|
||||
worktree_path: None,
|
||||
branch_name: None,
|
||||
detected_at: now,
|
||||
processed_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if a ticket with (tracker_id, artifact_id) already exists.
|
||||
#[cfg(test)]
|
||||
pub fn exists(conn: &Connection, tracker_id: &str, artifact_id: i32) -> Result<bool> {
|
||||
|
|
@ -119,15 +169,11 @@ impl ProcessedTicket {
|
|||
}
|
||||
|
||||
pub fn list_by_project(conn: &Connection, project_id: &str) -> Result<Vec<ProcessedTicket>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT pt.id, pt.tracker_id, pt.artifact_id, pt.artifact_title, pt.artifact_data, \
|
||||
pt.status, pt.analyst_report, pt.developer_report, pt.worktree_path, pt.branch_name, \
|
||||
pt.detected_at, pt.processed_at \
|
||||
FROM processed_tickets pt \
|
||||
JOIN watched_trackers wt ON pt.tracker_id = wt.id \
|
||||
WHERE wt.project_id = ?1 \
|
||||
ORDER BY pt.detected_at DESC",
|
||||
)?;
|
||||
let sql = format!(
|
||||
"{} WHERE project_id = ?1 ORDER BY detected_at DESC",
|
||||
SELECT_ALL_COLS
|
||||
);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map(params![project_id], from_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
|
@ -174,8 +220,7 @@ impl ProcessedTicket {
|
|||
END
|
||||
) AS avg_lead_time_seconds
|
||||
FROM processed_tickets pt
|
||||
JOIN watched_trackers wt ON wt.id = pt.tracker_id
|
||||
WHERE wt.project_id = ?1",
|
||||
WHERE pt.project_id = ?1",
|
||||
params![project_id, window_start],
|
||||
|row| {
|
||||
Ok(ProjectThroughputStats {
|
||||
|
|
@ -259,7 +304,7 @@ mod tests {
|
|||
use crate::models::project::Project;
|
||||
use crate::models::tracker::{NewWatchedTracker, WatchedTracker};
|
||||
|
||||
fn setup() -> (Connection, String) {
|
||||
fn setup() -> (Connection, String, String) {
|
||||
let conn = db::init_in_memory().expect("db init should succeed");
|
||||
let project = Project::insert(&conn, "Test", "/path", None, "main").unwrap();
|
||||
let analyst = Agent::insert(&conn, "A", AgentRole::Analyst, AgentTool::Codex, "").unwrap();
|
||||
|
|
@ -278,20 +323,12 @@ mod tests {
|
|||
},
|
||||
)
|
||||
.unwrap();
|
||||
(conn, tracker.id)
|
||||
}
|
||||
|
||||
fn project_id_for_tracker(conn: &Connection, tracker_id: &str) -> String {
|
||||
conn.query_row(
|
||||
"SELECT project_id FROM watched_trackers WHERE id = ?1",
|
||||
params![tracker_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap()
|
||||
(conn, project.id, tracker.id)
|
||||
}
|
||||
|
||||
fn insert_ticket_with_timestamps(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
tracker_id: &str,
|
||||
artifact_id: i32,
|
||||
status: &str,
|
||||
|
|
@ -300,10 +337,11 @@ mod tests {
|
|||
) {
|
||||
conn.execute(
|
||||
"INSERT INTO processed_tickets \
|
||||
(id, tracker_id, artifact_id, artifact_title, artifact_data, status, detected_at, processed_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
(id, project_id, tracker_id, source, source_ref, artifact_id, artifact_title, artifact_data, status, detected_at, processed_at) \
|
||||
VALUES (?1, ?2, ?3, 'tuleap', NULL, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
Uuid::new_v4().to_string(),
|
||||
project_id,
|
||||
tracker_id,
|
||||
artifact_id,
|
||||
format!("Ticket {}", artifact_id),
|
||||
|
|
@ -318,11 +356,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_insert_if_new_creates_ticket() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let expected_project_id = project_id_for_tracker(&conn, &tracker_id);
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
|
||||
let result = ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
101,
|
||||
"Fix login bug",
|
||||
|
|
@ -333,28 +371,70 @@ mod tests {
|
|||
assert!(result.is_some());
|
||||
let ticket = result.unwrap();
|
||||
assert_eq!(ticket.status, "Pending");
|
||||
assert_eq!(ticket.tracker_id, tracker_id);
|
||||
assert_eq!(ticket.project_id, project_id);
|
||||
assert_eq!(ticket.source, "tuleap");
|
||||
assert!(ticket.source_ref.is_none());
|
||||
assert_eq!(ticket.tracker_id.as_deref(), Some(tracker_id.as_str()));
|
||||
assert_eq!(ticket.artifact_id, 101);
|
||||
assert_eq!(ticket.artifact_title, "Fix login bug");
|
||||
assert!(ticket.analyst_report.is_none());
|
||||
assert!(ticket.processed_at.is_none());
|
||||
|
||||
let persisted_project_id: Option<String> = conn
|
||||
let persisted: (String, String, Option<String>, Option<String>) = conn
|
||||
.query_row(
|
||||
"SELECT project_id FROM processed_tickets WHERE id = ?1",
|
||||
"SELECT project_id, source, source_ref, tracker_id FROM processed_tickets WHERE id = ?1",
|
||||
params![ticket.id],
|
||||
|row| row.get(0),
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)
|
||||
.expect("project_id query should succeed");
|
||||
assert_eq!(persisted_project_id, Some(expected_project_id));
|
||||
.expect("processed ticket query should succeed");
|
||||
assert_eq!(persisted.0, project_id);
|
||||
assert_eq!(persisted.1, "tuleap");
|
||||
assert_eq!(persisted.2, None);
|
||||
assert_eq!(persisted.3, Some(tracker_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_external_creates_graylog_ticket() {
|
||||
let (conn, project_id, _) = setup();
|
||||
|
||||
let ticket = ProcessedTicket::insert_external(
|
||||
&conn,
|
||||
&project_id,
|
||||
"graylog",
|
||||
Some("message:abc123"),
|
||||
501,
|
||||
"Graylog alert",
|
||||
"{\"message_id\":\"abc123\"}",
|
||||
)
|
||||
.expect("insert_external should succeed");
|
||||
|
||||
assert_eq!(ticket.project_id, project_id);
|
||||
assert_eq!(ticket.source, "graylog");
|
||||
assert_eq!(ticket.source_ref.as_deref(), Some("message:abc123"));
|
||||
assert!(ticket.tracker_id.is_none());
|
||||
assert_eq!(ticket.status, "Pending");
|
||||
assert_eq!(ticket.artifact_id, 501);
|
||||
|
||||
let persisted: (String, String, Option<String>, Option<String>) = conn
|
||||
.query_row(
|
||||
"SELECT project_id, source, source_ref, tracker_id FROM processed_tickets WHERE id = ?1",
|
||||
params![ticket.id],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)
|
||||
.expect("processed ticket query should succeed");
|
||||
assert_eq!(persisted.0, project_id);
|
||||
assert_eq!(persisted.1, "graylog");
|
||||
assert_eq!(persisted.2, Some("message:abc123".to_string()));
|
||||
assert_eq!(persisted.3, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_if_new_returns_none_for_duplicate() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
|
||||
let first = ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
202,
|
||||
"Crash on startup",
|
||||
|
|
@ -365,6 +445,7 @@ mod tests {
|
|||
|
||||
let second = ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
202,
|
||||
"Crash on startup",
|
||||
|
|
@ -376,16 +457,24 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_unique_constraint_blocks_manual_duplicate_insert() {
|
||||
let (conn, tracker_id) = setup();
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker_id, 909, "Duplicate candidate", "{}")
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
909,
|
||||
"Duplicate candidate",
|
||||
"{}",
|
||||
)
|
||||
.expect("first insert should succeed");
|
||||
|
||||
let duplicate_insert = conn.execute(
|
||||
"INSERT INTO processed_tickets \
|
||||
(id, tracker_id, artifact_id, artifact_title, artifact_data, status, detected_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 'Pending', ?6)",
|
||||
(id, project_id, tracker_id, source, source_ref, artifact_id, artifact_title, artifact_data, status, detected_at) \
|
||||
VALUES (?1, ?2, ?3, 'tuleap', NULL, ?4, ?5, ?6, 'Pending', ?7)",
|
||||
rusqlite::params![
|
||||
Uuid::new_v4().to_string(),
|
||||
project_id,
|
||||
tracker_id,
|
||||
909,
|
||||
"Duplicate candidate",
|
||||
|
|
@ -407,13 +496,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_exists() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
|
||||
let before =
|
||||
ProcessedTicket::exists(&conn, &tracker_id, 303).expect("exists check should succeed");
|
||||
assert!(!before);
|
||||
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker_id, 303, "Some ticket", "{}")
|
||||
ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 303, "Some ticket", "{}")
|
||||
.expect("insert should succeed");
|
||||
|
||||
let after = ProcessedTicket::exists(&conn, &tracker_id, 303)
|
||||
|
|
@ -423,10 +512,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_list_by_tracker() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "Ticket One", "{}").unwrap();
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker_id, 2, "Ticket Two", "{}").unwrap();
|
||||
ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "Ticket One", "{}")
|
||||
.unwrap();
|
||||
ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 2, "Ticket Two", "{}")
|
||||
.unwrap();
|
||||
|
||||
let tickets =
|
||||
ProcessedTicket::list_by_tracker(&conn, &tracker_id).expect("list should succeed");
|
||||
|
|
@ -435,10 +526,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_get_by_id() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
|
||||
let inserted = ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
404,
|
||||
"Not Found Bug",
|
||||
|
|
@ -458,8 +550,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_update_status() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "T1", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -470,8 +562,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_set_analyst_report() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "T1", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -482,8 +574,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_set_developer_report() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "T1", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -502,8 +594,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_set_worktree_info() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "T1", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -515,9 +607,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_list_pending() {
|
||||
let (conn, tracker_id) = setup();
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}").unwrap();
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker_id, 2, "T2", "{}").unwrap();
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "T1", "{}").unwrap();
|
||||
ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 2, "T2", "{}").unwrap();
|
||||
|
||||
let pending = ProcessedTicket::list_pending(&conn).unwrap();
|
||||
assert_eq!(pending.len(), 2);
|
||||
|
|
@ -532,8 +624,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_set_error() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "T1", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -553,8 +645,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_get_project_throughput_stats() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let project_id = project_id_for_tracker(&conn, &tracker_id);
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
let now = chrono::Utc::now();
|
||||
let pending_detected = (now - chrono::Duration::hours(2)).to_rfc3339();
|
||||
let developing_detected = (now - chrono::Duration::hours(3)).to_rfc3339();
|
||||
|
|
@ -566,9 +657,18 @@ mod tests {
|
|||
let old_done_processed = (now - chrono::Duration::hours(35)).to_rfc3339();
|
||||
let cancelled_detected = (now - chrono::Duration::hours(1)).to_rfc3339();
|
||||
|
||||
insert_ticket_with_timestamps(&conn, &tracker_id, 1001, "Pending", &pending_detected, None);
|
||||
insert_ticket_with_timestamps(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
1001,
|
||||
"Pending",
|
||||
&pending_detected,
|
||||
None,
|
||||
);
|
||||
insert_ticket_with_timestamps(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
1002,
|
||||
"Developing",
|
||||
|
|
@ -577,6 +677,7 @@ mod tests {
|
|||
);
|
||||
insert_ticket_with_timestamps(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
1003,
|
||||
"Done",
|
||||
|
|
@ -585,6 +686,7 @@ mod tests {
|
|||
);
|
||||
insert_ticket_with_timestamps(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
1004,
|
||||
"Error",
|
||||
|
|
@ -593,6 +695,7 @@ mod tests {
|
|||
);
|
||||
insert_ticket_with_timestamps(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
1005,
|
||||
"Done",
|
||||
|
|
@ -601,6 +704,7 @@ mod tests {
|
|||
);
|
||||
insert_ticket_with_timestamps(
|
||||
&conn,
|
||||
&project_id,
|
||||
&tracker_id,
|
||||
1006,
|
||||
"Cancelled",
|
||||
|
|
@ -622,8 +726,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_get_project_throughput_stats_empty() {
|
||||
let (conn, tracker_id) = setup();
|
||||
let project_id = project_id_for_tracker(&conn, &tracker_id);
|
||||
let (conn, project_id, _) = setup();
|
||||
|
||||
let stats = ProcessedTicket::get_project_throughput_stats(&conn, &project_id).unwrap();
|
||||
assert_eq!(stats.backlog_count, 0);
|
||||
|
|
@ -631,4 +734,27 @@ mod tests {
|
|||
assert_eq!(stats.error_last_24h, 0);
|
||||
assert!(stats.avg_lead_time_seconds.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_by_project_includes_external_tickets_without_tracker_join() {
|
||||
let (conn, project_id, tracker_id) = setup();
|
||||
|
||||
ProcessedTicket::insert_if_new(&conn, &project_id, &tracker_id, 1, "Tuleap ticket", "{}")
|
||||
.unwrap();
|
||||
ProcessedTicket::insert_external(
|
||||
&conn,
|
||||
&project_id,
|
||||
"graylog",
|
||||
Some("event-1"),
|
||||
2,
|
||||
"Graylog ticket",
|
||||
"{}",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tickets = ProcessedTicket::list_by_project(&conn, &project_id).unwrap();
|
||||
assert_eq!(tickets.len(), 2);
|
||||
assert!(tickets.iter().any(|ticket| ticket.source == "graylog"));
|
||||
assert!(tickets.iter().any(|ticket| ticket.source == "tuleap"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,8 @@ mod tests {
|
|||
},
|
||||
)
|
||||
.unwrap();
|
||||
let ticket = ProcessedTicket::insert_if_new(&conn, &tracker.id, 42, "Bug 42", "{}")
|
||||
let ticket =
|
||||
ProcessedTicket::insert_if_new(&conn, &project.id, &tracker.id, 42, "Bug 42", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
(conn, ticket.id)
|
||||
|
|
@ -184,10 +185,10 @@ mod tests {
|
|||
},
|
||||
)
|
||||
.unwrap();
|
||||
let t1 = ProcessedTicket::insert_if_new(&conn, &tracker.id, 1, "T1", "{}")
|
||||
let t1 = ProcessedTicket::insert_if_new(&conn, &project.id, &tracker.id, 1, "T1", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let t2 = ProcessedTicket::insert_if_new(&conn, &tracker.id, 2, "T2", "{}")
|
||||
let t2 = ProcessedTicket::insert_if_new(&conn, &project.id, &tracker.id, 2, "T2", "{}")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,14 @@ mod tests {
|
|||
.expect("tracker insert should succeed");
|
||||
|
||||
let ticket =
|
||||
ProcessedTicket::insert_if_new(&conn, &tracker.id, 1, "Ticket 1", "{\"id\":1}")
|
||||
ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
project_id,
|
||||
&tracker.id,
|
||||
1,
|
||||
"Ticket 1",
|
||||
"{\"id\":1}",
|
||||
)
|
||||
.expect("ticket insert should succeed")
|
||||
.expect("ticket should be inserted");
|
||||
|
||||
|
|
|
|||
|
|
@ -262,7 +262,11 @@ async fn process_ticket(
|
|||
let mut selected: Option<(ProcessedTicket, WatchedTracker, Project)> = None;
|
||||
|
||||
for ticket in pending {
|
||||
let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)
|
||||
let Some(tracker_id) = ticket.tracker_id.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let tracker = WatchedTracker::get_by_id(&conn, tracker_id)
|
||||
.map_err(|e| format!("get tracker failed: {}", e))?;
|
||||
let project = Project::get_by_id(&conn, &tracker.project_id)
|
||||
.map_err(|e| format!("get project failed: {}", e))?;
|
||||
|
|
@ -603,7 +607,10 @@ mod tests {
|
|||
fn test_build_analyst_prompt_contains_ticket_info() {
|
||||
let ticket = ProcessedTicket {
|
||||
id: "t1".into(),
|
||||
tracker_id: "tr1".into(),
|
||||
tracker_id: Some("tr1".into()),
|
||||
project_id: "p1".into(),
|
||||
source: "tuleap".into(),
|
||||
source_ref: None,
|
||||
artifact_id: 42,
|
||||
artifact_title: "Login crash on empty password".into(),
|
||||
artifact_data: r#"{"id":42,"title":"Login crash"}"#.into(),
|
||||
|
|
@ -638,7 +645,10 @@ mod tests {
|
|||
fn test_build_developer_prompt_contains_report() {
|
||||
let ticket = ProcessedTicket {
|
||||
id: "t1".into(),
|
||||
tracker_id: "tr1".into(),
|
||||
tracker_id: Some("tr1".into()),
|
||||
project_id: "p1".into(),
|
||||
source: "tuleap".into(),
|
||||
source_ref: None,
|
||||
artifact_id: 42,
|
||||
artifact_title: "Login crash".into(),
|
||||
artifact_data: "{}".into(),
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ async fn poll_single_tracker(
|
|||
|
||||
match ProcessedTicket::insert_if_new(
|
||||
&conn,
|
||||
&tracker.project_id,
|
||||
&tracker.id,
|
||||
artifact_id,
|
||||
&artifact_title,
|
||||
|
|
|
|||
Loading…
Reference in a new issue