diff --git a/src-tauri/src/commands/tracker.rs b/src-tauri/src/commands/tracker.rs index b553373..bb368fd 100644 --- a/src-tauri/src/commands/tracker.rs +++ b/src-tauri/src/commands/tracker.rs @@ -2,10 +2,11 @@ use crate::error::AppError; use crate::models::agent::{Agent, AgentRole}; use crate::models::credential::TuleapCredentials; use crate::models::ticket::ProcessedTicket; -use crate::models::tracker::{FilterGroup, TrackerUpdate, WatchedTracker}; +use crate::models::tracker::{FilterGroup, NewWatchedTracker, TrackerUpdate, WatchedTracker}; use crate::services::crypto; use crate::services::tuleap_client::TuleapClient; use crate::AppState; +use serde::Deserialize; use tauri::State; fn build_tuleap_client(state: &State) -> Result { @@ -17,8 +18,8 @@ fn build_tuleap_client(state: &State) -> Result) -> Result Result<(), AppError> { +fn ensure_agent_role( + db: &rusqlite::Connection, + agent_id: &str, + expected: AgentRole, +) -> Result<(), AppError> { let agent = Agent::get_by_id(db, agent_id)?; if agent.role != expected { return Err(AppError::from(format!( @@ -41,34 +46,42 @@ fn ensure_agent_role(db: &rusqlite::Connection, agent_id: &str, expected: AgentR Ok(()) } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddTrackerPayload { + pub project_id: String, + pub tracker_id: i32, + pub tracker_label: String, + pub polling_interval: i32, + pub analyst_agent_id: String, + pub developer_agent_id: String, + pub filters: Vec, +} + #[tauri::command] pub fn add_tracker( state: State<'_, AppState>, - project_id: String, - tracker_id: i32, - tracker_label: String, - polling_interval: i32, - analyst_agent_id: String, - developer_agent_id: String, - filters: Vec, + payload: AddTrackerPayload, ) -> Result { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; - ensure_agent_role(&db, &analyst_agent_id, AgentRole::Analyst)?; - ensure_agent_role(&db, &developer_agent_id, AgentRole::Developer)?; + ensure_agent_role(&db, &payload.analyst_agent_id, AgentRole::Analyst)?; + ensure_agent_role(&db, &payload.developer_agent_id, AgentRole::Developer)?; let tracker = WatchedTracker::insert( &db, - &project_id, - tracker_id, - &tracker_label, - polling_interval, - &analyst_agent_id, - &developer_agent_id, - filters, + NewWatchedTracker { + project_id: payload.project_id, + tracker_id: payload.tracker_id, + tracker_label: payload.tracker_label, + polling_interval: payload.polling_interval, + analyst_agent_id: payload.analyst_agent_id, + developer_agent_id: payload.developer_agent_id, + filters: payload.filters, + }, )?; Ok(tracker) diff --git a/src-tauri/src/models/agent.rs b/src-tauri/src/models/agent.rs index fc5f5be..d43f4b5 100644 --- a/src-tauri/src/models/agent.rs +++ b/src-tauri/src/models/agent.rs @@ -247,7 +247,7 @@ mod tests { use super::*; use crate::db; use crate::models::project::Project; - use crate::models::tracker::WatchedTracker; + use crate::models::tracker::{NewWatchedTracker, WatchedTracker}; fn setup() -> Connection { db::init_in_memory().expect("db init should succeed") @@ -302,10 +302,9 @@ mod tests { "new script", ) .unwrap_err(); - assert!( - err.to_string() - .contains("Default agents cannot change name, role, or tool") - ); + assert!(err + .to_string() + .contains("Default agents cannot change name, role, or tool")); Agent::update( &conn, @@ -338,18 +337,20 @@ mod tests { let analyst_default = Agent::get_default_by_role(&conn, AgentRole::Analyst).unwrap(); let developer_default = Agent::get_default_by_role(&conn, AgentRole::Developer).unwrap(); - let analyst = Agent::insert(&conn, "Analyst", AgentRole::Analyst, AgentTool::Codex, "") - .unwrap(); + let analyst = + Agent::insert(&conn, "Analyst", AgentRole::Analyst, AgentTool::Codex, "").unwrap(); let tracker = WatchedTracker::insert( &conn, - &project.id, - 100, - "Bugs", - 10, - &analyst.id, - &developer_default.id, - vec![], + NewWatchedTracker { + project_id: project.id.clone(), + tracker_id: 100, + tracker_label: "Bugs".to_string(), + polling_interval: 10, + analyst_agent_id: analyst.id.clone(), + developer_agent_id: developer_default.id.clone(), + filters: vec![], + }, ) .unwrap(); diff --git a/src-tauri/src/models/ticket.rs b/src-tauri/src/models/ticket.rs index 3db7367..7a973e1 100644 --- a/src-tauri/src/models/ticket.rs +++ b/src-tauri/src/models/ticket.rs @@ -60,7 +60,14 @@ impl ProcessedTicket { "INSERT INTO processed_tickets \ (id, tracker_id, artifact_id, artifact_title, artifact_data, status, detected_at) \ VALUES (?1, ?2, ?3, ?4, ?5, 'Pending', ?6)", - params![id, tracker_id, artifact_id, artifact_title, artifact_data, now], + params![ + id, + tracker_id, + artifact_id, + artifact_title, + artifact_data, + now + ], )?; let ticket = ProcessedTicket { @@ -183,7 +190,7 @@ mod tests { use crate::db; use crate::models::agent::{Agent, AgentRole, AgentTool}; use crate::models::project::Project; - use crate::models::tracker::WatchedTracker; + use crate::models::tracker::{NewWatchedTracker, WatchedTracker}; fn setup() -> (Connection, String) { let conn = db::init_in_memory().expect("db init should succeed"); @@ -193,13 +200,15 @@ mod tests { Agent::insert(&conn, "D", AgentRole::Developer, AgentTool::ClaudeCode, "").unwrap(); let tracker = WatchedTracker::insert( &conn, - &project.id, - 456, - "Bugs", - 10, - &analyst.id, - &developer.id, - vec![], + NewWatchedTracker { + project_id: project.id.clone(), + tracker_id: 456, + tracker_label: "Bugs".to_string(), + polling_interval: 10, + analyst_agent_id: analyst.id.clone(), + developer_agent_id: developer.id.clone(), + filters: vec![], + }, ) .unwrap(); (conn, tracker.id) @@ -257,8 +266,8 @@ mod tests { fn test_exists() { let (conn, tracker_id) = setup(); - let before = ProcessedTicket::exists(&conn, &tracker_id, 303) - .expect("exists check should succeed"); + 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", "{}") @@ -285,10 +294,15 @@ mod tests { fn test_get_by_id() { let (conn, tracker_id) = setup(); - let inserted = - ProcessedTicket::insert_if_new(&conn, &tracker_id, 404, "Not Found Bug", "{\"id\": 404}") - .expect("insert should succeed") - .expect("should be Some"); + let inserted = ProcessedTicket::insert_if_new( + &conn, + &tracker_id, + 404, + "Not Found Bug", + "{\"id\": 404}", + ) + .expect("insert should succeed") + .expect("should be Some"); let found = ProcessedTicket::get_by_id(&conn, &inserted.id).expect("get_by_id should succeed"); diff --git a/src-tauri/src/models/tracker.rs b/src-tauri/src/models/tracker.rs index 059d10d..73e4012 100644 --- a/src-tauri/src/models/tracker.rs +++ b/src-tauri/src/models/tracker.rs @@ -41,6 +41,17 @@ pub struct TrackerUpdate { pub enabled: bool, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewWatchedTracker { + pub project_id: String, + pub tracker_id: i32, + pub tracker_label: String, + pub polling_interval: i32, + pub analyst_agent_id: String, + pub developer_agent_id: String, + pub filters: Vec, +} + fn normalize_agent_id(agent_id: &str) -> Option { let trimmed = agent_id.trim(); if trimmed.is_empty() { @@ -50,7 +61,10 @@ fn normalize_agent_id(agent_id: &str) -> Option { } } -fn compute_status(analyst_agent_id: &Option, developer_agent_id: &Option) -> String { +fn compute_status( + analyst_agent_id: &Option, + developer_agent_id: &Option, +) -> String { if analyst_agent_id.is_some() && developer_agent_id.is_some() { "valid".to_string() } else { @@ -62,8 +76,9 @@ fn from_row(row: &rusqlite::Row) -> rusqlite::Result { let filters_json: String = row.get(7)?; let enabled_int: i32 = row.get(8)?; - let filters: Vec = serde_json::from_str(&filters_json) - .map_err(|e| rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e)))?; + let filters: Vec = serde_json::from_str(&filters_json).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e)) + })?; Ok(WatchedTracker { id: row.get(0)?, @@ -82,49 +97,50 @@ fn from_row(row: &rusqlite::Row) -> rusqlite::Result { } impl WatchedTracker { - pub fn insert( - conn: &Connection, - project_id: &str, - tracker_id: i32, - tracker_label: &str, - polling_interval: i32, - analyst_agent_id: &str, - developer_agent_id: &str, - filters: Vec, - ) -> Result { + pub fn insert(conn: &Connection, new_tracker: NewWatchedTracker) -> Result { + let NewWatchedTracker { + project_id, + tracker_id, + tracker_label, + polling_interval, + analyst_agent_id, + developer_agent_id, + filters, + } = new_tracker; + let id = Uuid::new_v4().to_string(); let now = chrono::Utc::now().to_rfc3339(); let filters_json = serde_json::to_string(&filters) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let analyst_agent_id = normalize_agent_id(analyst_agent_id); - let developer_agent_id = normalize_agent_id(developer_agent_id); + let analyst_agent_id = normalize_agent_id(&analyst_agent_id); + let developer_agent_id = normalize_agent_id(&developer_agent_id); let status = compute_status(&analyst_agent_id, &developer_agent_id); conn.execute( "INSERT INTO watched_trackers (id, project_id, tracker_id, tracker_label, polling_interval, agent_config_json, filters_json, analyst_agent_id, developer_agent_id, status, created_at) \ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![ - id, - project_id, + &id, + &project_id, tracker_id, - tracker_label, + &tracker_label, polling_interval, "{}", filters_json, - analyst_agent_id, - developer_agent_id, - status, - now, + analyst_agent_id.as_deref(), + developer_agent_id.as_deref(), + &status, + &now, ], )?; Ok(WatchedTracker { id, - project_id: project_id.to_string(), + project_id, tracker_id, - tracker_label: tracker_label.to_string(), + tracker_label, polling_interval, analyst_agent_id, developer_agent_id, @@ -232,14 +248,8 @@ mod tests { } fn create_agents(conn: &Connection) -> (String, String) { - let analyst = Agent::insert( - conn, - "Analyst", - AgentRole::Analyst, - AgentTool::Codex, - "", - ) - .unwrap(); + let analyst = + Agent::insert(conn, "Analyst", AgentRole::Analyst, AgentTool::Codex, "").unwrap(); let developer = Agent::insert( conn, @@ -271,13 +281,15 @@ mod tests { let tracker = WatchedTracker::insert( &conn, - &pid, - 42, - "Bug Tracker", - 15, - &analyst_id, - &developer_id, - sample_filters(), + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 42, + tracker_label: "Bug Tracker".to_string(), + polling_interval: 15, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: sample_filters(), + }, ) .expect("insert should succeed"); @@ -288,8 +300,14 @@ mod tests { assert_eq!(tracker.polling_interval, 15); assert!(tracker.enabled); assert_eq!(tracker.status, "valid"); - assert_eq!(tracker.analyst_agent_id.as_deref(), Some(analyst_id.as_str())); - assert_eq!(tracker.developer_agent_id.as_deref(), Some(developer_id.as_str())); + assert_eq!( + tracker.analyst_agent_id.as_deref(), + Some(analyst_id.as_str()) + ); + assert_eq!( + tracker.developer_agent_id.as_deref(), + Some(developer_id.as_str()) + ); assert!(tracker.last_polled_at.is_none()); assert!(!tracker.created_at.is_empty()); assert_eq!(tracker.filters.len(), 1); @@ -301,8 +319,32 @@ mod tests { let pid = project_id(&conn); let (analyst_id, developer_id) = create_agents(&conn); - WatchedTracker::insert(&conn, &pid, 1, "Tracker A", 10, &analyst_id, &developer_id, vec![]).unwrap(); - WatchedTracker::insert(&conn, &pid, 2, "Tracker B", 20, &analyst_id, &developer_id, vec![]).unwrap(); + WatchedTracker::insert( + &conn, + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 1, + tracker_label: "Tracker A".to_string(), + polling_interval: 10, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: vec![], + }, + ) + .unwrap(); + WatchedTracker::insert( + &conn, + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 2, + tracker_label: "Tracker B".to_string(), + polling_interval: 20, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: vec![], + }, + ) + .unwrap(); let trackers = WatchedTracker::list_by_project(&conn, &pid).expect("list should succeed"); assert_eq!(trackers.len(), 2); @@ -314,10 +356,35 @@ mod tests { let pid = project_id(&conn); let (analyst_id, developer_id) = create_agents(&conn); - let valid = WatchedTracker::insert(&conn, &pid, 1, "Valid", 10, &analyst_id, &developer_id, vec![]).unwrap(); - WatchedTracker::insert(&conn, &pid, 2, "Invalid", 10, "", &developer_id, vec![]).unwrap(); + let valid = WatchedTracker::insert( + &conn, + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 1, + tracker_label: "Valid".to_string(), + polling_interval: 10, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: vec![], + }, + ) + .unwrap(); + WatchedTracker::insert( + &conn, + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 2, + tracker_label: "Invalid".to_string(), + polling_interval: 10, + analyst_agent_id: "".to_string(), + developer_agent_id: developer_id.clone(), + filters: vec![], + }, + ) + .unwrap(); - let enabled = WatchedTracker::list_all_enabled(&conn).expect("list_all_enabled should succeed"); + let enabled = + WatchedTracker::list_all_enabled(&conn).expect("list_all_enabled should succeed"); assert_eq!(enabled.len(), 1); assert_eq!(enabled[0].id, valid.id); } @@ -330,17 +397,20 @@ mod tests { let created = WatchedTracker::insert( &conn, - &pid, - 99, - "My Tracker", - 30, - &analyst_id, - &developer_id, - sample_filters(), + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 99, + tracker_label: "My Tracker".to_string(), + polling_interval: 30, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: sample_filters(), + }, ) .unwrap(); - let found = WatchedTracker::get_by_id(&conn, &created.id).expect("get_by_id should succeed"); + let found = + WatchedTracker::get_by_id(&conn, &created.id).expect("get_by_id should succeed"); assert_eq!(found.id, created.id); assert_eq!(found.tracker_id, 99); assert_eq!(found.tracker_label, "My Tracker"); @@ -356,13 +426,15 @@ mod tests { let created = WatchedTracker::insert( &conn, - &pid, - 10, - "Original", - 5, - &analyst_id, - &developer_id, - sample_filters(), + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 10, + tracker_label: "Original".to_string(), + polling_interval: 5, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: sample_filters(), + }, ) .unwrap(); @@ -406,19 +478,22 @@ mod tests { let created = WatchedTracker::insert( &conn, - &pid, - 5, - "Poller", - 10, - &analyst_id, - &developer_id, - vec![], + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 5, + tracker_label: "Poller".to_string(), + polling_interval: 10, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: vec![], + }, ) .unwrap(); assert!(created.last_polled_at.is_none()); - WatchedTracker::update_last_polled(&conn, &created.id).expect("update_last_polled should succeed"); + WatchedTracker::update_last_polled(&conn, &created.id) + .expect("update_last_polled should succeed"); let updated = WatchedTracker::get_by_id(&conn, &created.id).unwrap(); assert!(updated.last_polled_at.is_some()); @@ -432,13 +507,15 @@ mod tests { let created = WatchedTracker::insert( &conn, - &pid, - 7, - "ToDelete", - 10, - &analyst_id, - &developer_id, - vec![], + NewWatchedTracker { + project_id: pid.clone(), + tracker_id: 7, + tracker_label: "ToDelete".to_string(), + polling_interval: 10, + analyst_agent_id: analyst_id.clone(), + developer_agent_id: developer_id.clone(), + filters: vec![], + }, ) .unwrap(); diff --git a/src-tauri/src/models/worktree.rs b/src-tauri/src/models/worktree.rs index 4e5179b..f704005 100644 --- a/src-tauri/src/models/worktree.rs +++ b/src-tauri/src/models/worktree.rs @@ -31,7 +31,12 @@ const SELECT_ALL_COLS: &str = "SELECT id, ticket_id, path, branch_name, status, created_at, merged_at, merged_into FROM worktrees"; impl Worktree { - pub fn insert(conn: &Connection, ticket_id: &str, path: &str, branch_name: &str) -> Result { + pub fn insert( + conn: &Connection, + ticket_id: &str, + path: &str, + branch_name: &str, + ) -> Result { let id = Uuid::new_v4().to_string(); let now = chrono::Utc::now().to_rfc3339(); @@ -103,7 +108,7 @@ mod tests { use crate::models::agent::{Agent, AgentRole, AgentTool}; use crate::models::project::Project; use crate::models::ticket::ProcessedTicket; - use crate::models::tracker::WatchedTracker; + use crate::models::tracker::{NewWatchedTracker, WatchedTracker}; fn setup() -> (Connection, String) { let conn = db::init_in_memory().expect("db init"); @@ -113,13 +118,15 @@ mod tests { Agent::insert(&conn, "D", AgentRole::Developer, AgentTool::ClaudeCode, "").unwrap(); let tracker = WatchedTracker::insert( &conn, - &project.id, - 100, - "Bugs", - 10, - &analyst.id, - &developer.id, - vec![], + NewWatchedTracker { + project_id: project.id.clone(), + tracker_id: 100, + tracker_label: "Bugs".to_string(), + polling_interval: 10, + analyst_agent_id: analyst.id.clone(), + developer_agent_id: developer.id.clone(), + filters: vec![], + }, ) .unwrap(); let ticket = ProcessedTicket::insert_if_new(&conn, &tracker.id, 42, "Bug 42", "{}") @@ -165,13 +172,15 @@ mod tests { Agent::insert(&conn, "D", AgentRole::Developer, AgentTool::ClaudeCode, "").unwrap(); let tracker = WatchedTracker::insert( &conn, - &project.id, - 100, - "Bugs", - 10, - &analyst.id, - &developer.id, - vec![], + NewWatchedTracker { + project_id: project.id.clone(), + tracker_id: 100, + tracker_label: "Bugs".to_string(), + polling_interval: 10, + analyst_agent_id: analyst.id.clone(), + developer_agent_id: developer.id.clone(), + filters: vec![], + }, ) .unwrap(); let t1 = ProcessedTicket::insert_if_new(&conn, &tracker.id, 1, "T1", "{}") diff --git a/src-tauri/src/services/notifier.rs b/src-tauri/src/services/notifier.rs index bb612fd..55b3e9b 100644 --- a/src-tauri/src/services/notifier.rs +++ b/src-tauri/src/services/notifier.rs @@ -142,7 +142,7 @@ mod tests { use crate::models::agent::{Agent, AgentRole, AgentTool}; use crate::models::project::Project; use crate::models::ticket::ProcessedTicket; - use crate::models::tracker::WatchedTracker; + use crate::models::tracker::{NewWatchedTracker, WatchedTracker}; fn setup() -> (Arc>, String) { let conn = db::init_in_memory().expect("db init should succeed"); @@ -159,25 +159,22 @@ mod tests { let tracker = WatchedTracker::insert( &conn, - project_id, - 101, - "Bugs", - 10, - &analyst.id, - &developer.id, - vec![], + NewWatchedTracker { + project_id: project_id.to_string(), + tracker_id: 101, + tracker_label: "Bugs".to_string(), + polling_interval: 10, + analyst_agent_id: analyst.id.clone(), + developer_agent_id: developer.id.clone(), + filters: vec![], + }, ) .expect("tracker insert should succeed"); - let ticket = ProcessedTicket::insert_if_new( - &conn, - &tracker.id, - 1, - "Ticket 1", - "{\"id\":1}", - ) - .expect("ticket insert should succeed") - .expect("ticket should be inserted"); + let ticket = + ProcessedTicket::insert_if_new(&conn, &tracker.id, 1, "Ticket 1", "{\"id\":1}") + .expect("ticket insert should succeed") + .expect("ticket should be inserted"); ticket.id } diff --git a/src/lib/api.ts b/src/lib/api.ts index 36aae83..6fa5218 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -99,13 +99,15 @@ export async function addTracker( filters: FilterGroup[] ): Promise { return invoke("add_tracker", { - projectId, - trackerId, - trackerLabel, - pollingInterval, - analystAgentId, - developerAgentId, - filters, + payload: { + projectId, + trackerId, + trackerLabel, + pollingInterval, + analystAgentId, + developerAgentId, + filters, + }, }); } export async function listTrackers(projectId: string): Promise {