feat(queue): support multi-source processed tickets

This commit is contained in:
thibaud-lclr 2026-04-17 15:09:02 +02:00
parent 0149e4ca97
commit fc434fe560
8 changed files with 247 additions and 111 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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);

View file

@ -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", "{}")
.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"));
}
}

View file

@ -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();

View file

@ -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");

View file

@ -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(),

View file

@ -204,6 +204,7 @@ async fn poll_single_tracker(
match ProcessedTicket::insert_if_new(
&conn,
&tracker.project_id,
&tracker.id,
artifact_id,
&artifact_title,