diff --git a/src-tauri/src/commands/orchestrator.rs b/src-tauri/src/commands/orchestrator.rs index 7b32c44..25a5ec2 100644 --- a/src-tauri/src/commands/orchestrator.rs +++ b/src-tauri/src/commands/orchestrator.rs @@ -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, diff --git a/src-tauri/src/commands/poller.rs b/src-tauri/src/commands/poller.rs index 10c68a2..3379d96 100644 --- a/src-tauri/src/commands/poller.rs +++ b/src-tauri/src/commands/poller.rs @@ -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, diff --git a/src-tauri/src/commands/worktree.rs b/src-tauri/src/commands/worktree.rs index 9d81297..6606adf 100644 --- a/src-tauri/src/commands/worktree.rs +++ b/src-tauri/src/commands/worktree.rs @@ -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); diff --git a/src-tauri/src/models/ticket.rs b/src-tauri/src/models/ticket.rs index f67eff4..87c50a5 100644 --- a/src-tauri/src/models/ticket.rs +++ b/src-tauri/src/models/ticket.rs @@ -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, + pub project_id: String, + pub source: String, + pub source_ref: Option, pub artifact_id: i32, pub artifact_title: String, pub artifact_data: String, @@ -30,28 +33,32 @@ fn from_row(row: &rusqlite::Row) -> rusqlite::Result { 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 { + 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 { @@ -119,15 +169,11 @@ impl ProcessedTicket { } pub fn list_by_project(conn: &Connection, project_id: &str) -> Result> { - 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 = conn + let persisted: (String, String, Option, Option) = 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, Option) = 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", "{}") - .expect("first insert should succeed"); + 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")); + } } diff --git a/src-tauri/src/models/worktree.rs b/src-tauri/src/models/worktree.rs index 709109c..529b7f9 100644 --- a/src-tauri/src/models/worktree.rs +++ b/src-tauri/src/models/worktree.rs @@ -130,9 +130,10 @@ mod tests { }, ) .unwrap(); - let ticket = ProcessedTicket::insert_if_new(&conn, &tracker.id, 42, "Bug 42", "{}") - .unwrap() - .unwrap(); + 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(); diff --git a/src-tauri/src/services/notifier.rs b/src-tauri/src/services/notifier.rs index 55b3e9b..070afd8 100644 --- a/src-tauri/src/services/notifier.rs +++ b/src-tauri/src/services/notifier.rs @@ -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"); diff --git a/src-tauri/src/services/orchestrator.rs b/src-tauri/src/services/orchestrator.rs index 4a4911b..3b06a1f 100644 --- a/src-tauri/src/services/orchestrator.rs +++ b/src-tauri/src/services/orchestrator.rs @@ -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(), diff --git a/src-tauri/src/services/poller.rs b/src-tauri/src/services/poller.rs index e68e83e..d820906 100644 --- a/src-tauri/src/services/poller.rs +++ b/src-tauri/src/services/poller.rs @@ -204,6 +204,7 @@ async fn poll_single_tracker( match ProcessedTicket::insert_if_new( &conn, + &tracker.project_id, &tracker.id, artifact_id, &artifact_title,