intent_engine/
events.rs

1use crate::db::models::Event;
2use crate::error::{IntentError, Result};
3use chrono::Utc;
4use sqlx::SqlitePool;
5
6pub struct EventManager<'a> {
7    pool: &'a SqlitePool,
8}
9
10impl<'a> EventManager<'a> {
11    pub fn new(pool: &'a SqlitePool) -> Self {
12        Self { pool }
13    }
14
15    /// Add a new event
16    pub async fn add_event(
17        &self,
18        task_id: i64,
19        log_type: &str,
20        discussion_data: &str,
21    ) -> Result<Event> {
22        // Check if task exists
23        let task_exists: bool =
24            sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
25                .bind(task_id)
26                .fetch_one(self.pool)
27                .await?;
28
29        if !task_exists {
30            return Err(IntentError::TaskNotFound(task_id));
31        }
32
33        let now = Utc::now();
34
35        let result = sqlx::query(
36            r#"
37            INSERT INTO events (task_id, log_type, discussion_data, timestamp)
38            VALUES (?, ?, ?, ?)
39            "#,
40        )
41        .bind(task_id)
42        .bind(log_type)
43        .bind(discussion_data)
44        .bind(now)
45        .execute(self.pool)
46        .await?;
47
48        let id = result.last_insert_rowid();
49
50        Ok(Event {
51            id,
52            task_id,
53            timestamp: now,
54            log_type: log_type.to_string(),
55            discussion_data: discussion_data.to_string(),
56        })
57    }
58
59    /// List events for a task
60    pub async fn list_events(
61        &self,
62        task_id: i64,
63        limit: Option<i64>,
64        log_type: Option<String>,
65        since: Option<String>,
66    ) -> Result<Vec<Event>> {
67        // Check if task exists
68        let task_exists: bool =
69            sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
70                .bind(task_id)
71                .fetch_one(self.pool)
72                .await?;
73
74        if !task_exists {
75            return Err(IntentError::TaskNotFound(task_id));
76        }
77
78        let limit = limit.unwrap_or(100);
79
80        // Parse since duration if provided
81        let since_timestamp = if let Some(duration_str) = since {
82            Some(Self::parse_duration(&duration_str)?)
83        } else {
84            None
85        };
86
87        // Build dynamic query based on filters
88        let mut query = String::from(
89            "SELECT id, task_id, timestamp, log_type, discussion_data FROM events WHERE task_id = ?",
90        );
91        let mut conditions = Vec::new();
92
93        if log_type.is_some() {
94            conditions.push("log_type = ?");
95        }
96
97        if since_timestamp.is_some() {
98            conditions.push("timestamp >= ?");
99        }
100
101        if !conditions.is_empty() {
102            query.push_str(" AND ");
103            query.push_str(&conditions.join(" AND "));
104        }
105
106        query.push_str(" ORDER BY timestamp DESC LIMIT ?");
107
108        // Build and execute query
109        let mut sql_query = sqlx::query_as::<_, Event>(&query).bind(task_id);
110
111        if let Some(ref typ) = log_type {
112            sql_query = sql_query.bind(typ);
113        }
114
115        if let Some(ts) = since_timestamp {
116            sql_query = sql_query.bind(ts);
117        }
118
119        sql_query = sql_query.bind(limit);
120
121        let events = sql_query.fetch_all(self.pool).await?;
122
123        Ok(events)
124    }
125
126    /// Parse duration string (e.g., "7d", "24h", "30m") into a DateTime
127    fn parse_duration(duration: &str) -> Result<chrono::DateTime<Utc>> {
128        let len = duration.len();
129        if len < 2 {
130            return Err(IntentError::InvalidInput(
131                "Duration must be in format like '7d', '24h', or '30m'".to_string(),
132            ));
133        }
134
135        let (num_str, unit) = duration.split_at(len - 1);
136        let num: i64 = num_str.parse().map_err(|_| {
137            IntentError::InvalidInput(format!("Invalid number in duration: {}", num_str))
138        })?;
139
140        let now = Utc::now();
141        let result = match unit {
142            "d" => now - chrono::Duration::days(num),
143            "h" => now - chrono::Duration::hours(num),
144            "m" => now - chrono::Duration::minutes(num),
145            "s" => now - chrono::Duration::seconds(num),
146            _ => {
147                return Err(IntentError::InvalidInput(format!(
148                    "Invalid duration unit '{}'. Use 'd' (days), 'h' (hours), 'm' (minutes), or 's' (seconds)",
149                    unit
150                )))
151            }
152        };
153
154        Ok(result)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::tasks::TaskManager;
162    use crate::test_utils::test_helpers::TestContext;
163
164    #[tokio::test]
165    async fn test_add_event() {
166        let ctx = TestContext::new().await;
167        let task_mgr = TaskManager::new(ctx.pool());
168        let event_mgr = EventManager::new(ctx.pool());
169
170        let task = task_mgr.add_task("Test task", None, None).await.unwrap();
171        let event = event_mgr
172            .add_event(task.id, "decision", "Test decision")
173            .await
174            .unwrap();
175
176        assert_eq!(event.task_id, task.id);
177        assert_eq!(event.log_type, "decision");
178        assert_eq!(event.discussion_data, "Test decision");
179    }
180
181    #[tokio::test]
182    async fn test_add_event_nonexistent_task() {
183        let ctx = TestContext::new().await;
184        let event_mgr = EventManager::new(ctx.pool());
185
186        let result = event_mgr.add_event(999, "decision", "Test").await;
187        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
188    }
189
190    #[tokio::test]
191    async fn test_list_events() {
192        let ctx = TestContext::new().await;
193        let task_mgr = TaskManager::new(ctx.pool());
194        let event_mgr = EventManager::new(ctx.pool());
195
196        let task = task_mgr.add_task("Test task", None, None).await.unwrap();
197
198        // Add multiple events
199        event_mgr
200            .add_event(task.id, "decision", "Decision 1")
201            .await
202            .unwrap();
203        event_mgr
204            .add_event(task.id, "blocker", "Blocker 1")
205            .await
206            .unwrap();
207        event_mgr
208            .add_event(task.id, "milestone", "Milestone 1")
209            .await
210            .unwrap();
211
212        let events = event_mgr
213            .list_events(task.id, None, None, None)
214            .await
215            .unwrap();
216        assert_eq!(events.len(), 3);
217
218        // Events should be in reverse chronological order
219        assert_eq!(events[0].log_type, "milestone");
220        assert_eq!(events[1].log_type, "blocker");
221        assert_eq!(events[2].log_type, "decision");
222    }
223
224    #[tokio::test]
225    async fn test_list_events_with_limit() {
226        let ctx = TestContext::new().await;
227        let task_mgr = TaskManager::new(ctx.pool());
228        let event_mgr = EventManager::new(ctx.pool());
229
230        let task = task_mgr.add_task("Test task", None, None).await.unwrap();
231
232        // Add 5 events
233        for i in 0..5 {
234            event_mgr
235                .add_event(task.id, "test", &format!("Event {}", i))
236                .await
237                .unwrap();
238        }
239
240        let events = event_mgr
241            .list_events(task.id, Some(3), None, None)
242            .await
243            .unwrap();
244        assert_eq!(events.len(), 3);
245    }
246
247    #[tokio::test]
248    async fn test_list_events_nonexistent_task() {
249        let ctx = TestContext::new().await;
250        let event_mgr = EventManager::new(ctx.pool());
251
252        let result = event_mgr.list_events(999, None, None, None).await;
253        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
254    }
255
256    #[tokio::test]
257    async fn test_list_events_empty() {
258        let ctx = TestContext::new().await;
259        let task_mgr = TaskManager::new(ctx.pool());
260        let event_mgr = EventManager::new(ctx.pool());
261
262        let task = task_mgr.add_task("Test task", None, None).await.unwrap();
263
264        let events = event_mgr
265            .list_events(task.id, None, None, None)
266            .await
267            .unwrap();
268        assert_eq!(events.len(), 0);
269    }
270}