Skip to main content

ai_chain/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5pub struct Issue {
6    pub id: i64,
7    pub title: String,
8    pub description: Option<String>,
9    pub status: String,
10    pub priority: String,
11    pub parent_id: Option<i64>,
12    pub created_at: DateTime<Utc>,
13    pub updated_at: DateTime<Utc>,
14    pub closed_at: Option<DateTime<Utc>>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct Comment {
19    pub id: i64,
20    pub issue_id: i64,
21    pub content: String,
22    pub created_at: DateTime<Utc>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct Session {
27    pub id: i64,
28    pub started_at: DateTime<Utc>,
29    pub ended_at: Option<DateTime<Utc>>,
30    pub active_issue_id: Option<i64>,
31    pub handoff_notes: Option<String>,
32    pub last_action: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct Milestone {
37    pub id: i64,
38    pub name: String,
39    pub description: Option<String>,
40    pub status: String,
41    pub created_at: DateTime<Utc>,
42    pub closed_at: Option<DateTime<Utc>>,
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use proptest::prelude::*;
49
50    // ==================== Issue Tests ====================
51
52    #[test]
53    fn test_issue_serialization_json() {
54        let issue = Issue {
55            id: 1,
56            title: "Test issue".to_string(),
57            description: Some("A description".to_string()),
58            status: "open".to_string(),
59            priority: "high".to_string(),
60            parent_id: None,
61            created_at: Utc::now(),
62            updated_at: Utc::now(),
63            closed_at: None,
64        };
65
66        let json = serde_json::to_string(&issue).unwrap();
67        let deserialized: Issue = serde_json::from_str(&json).unwrap();
68
69        assert_eq!(issue.id, deserialized.id);
70        assert_eq!(issue.title, deserialized.title);
71        assert_eq!(issue.description, deserialized.description);
72        assert_eq!(issue.status, deserialized.status);
73        assert_eq!(issue.priority, deserialized.priority);
74        assert_eq!(issue.parent_id, deserialized.parent_id);
75    }
76
77    #[test]
78    fn test_issue_with_parent() {
79        let issue = Issue {
80            id: 2,
81            title: "Child issue".to_string(),
82            description: None,
83            status: "open".to_string(),
84            priority: "medium".to_string(),
85            parent_id: Some(1),
86            created_at: Utc::now(),
87            updated_at: Utc::now(),
88            closed_at: None,
89        };
90
91        let json = serde_json::to_string(&issue).unwrap();
92        let deserialized: Issue = serde_json::from_str(&json).unwrap();
93
94        assert_eq!(deserialized.parent_id, Some(1));
95    }
96
97    #[test]
98    fn test_issue_closed_at() {
99        let now = Utc::now();
100        let issue = Issue {
101            id: 1,
102            title: "Closed issue".to_string(),
103            description: None,
104            status: "closed".to_string(),
105            priority: "low".to_string(),
106            parent_id: None,
107            created_at: now,
108            updated_at: now,
109            closed_at: Some(now),
110        };
111
112        let json = serde_json::to_string(&issue).unwrap();
113        let deserialized: Issue = serde_json::from_str(&json).unwrap();
114
115        assert_eq!(deserialized.closed_at, Some(now));
116    }
117
118    #[test]
119    fn test_issue_unicode_fields() {
120        let issue = Issue {
121            id: 1,
122            title: "测试 🐛 αβγ".to_string(),
123            description: Some("Description with émojis 🎉".to_string()),
124            status: "open".to_string(),
125            priority: "high".to_string(),
126            parent_id: None,
127            created_at: Utc::now(),
128            updated_at: Utc::now(),
129            closed_at: None,
130        };
131
132        let json = serde_json::to_string(&issue).unwrap();
133        let deserialized: Issue = serde_json::from_str(&json).unwrap();
134
135        assert_eq!(deserialized.title, "测试 🐛 αβγ");
136        assert_eq!(
137            deserialized.description,
138            Some("Description with émojis 🎉".to_string())
139        );
140    }
141
142    // ==================== Comment Tests ====================
143
144    #[test]
145    fn test_comment_serialization() {
146        let comment = Comment {
147            id: 1,
148            issue_id: 42,
149            content: "A comment".to_string(),
150            created_at: Utc::now(),
151        };
152
153        let json = serde_json::to_string(&comment).unwrap();
154        let deserialized: Comment = serde_json::from_str(&json).unwrap();
155
156        assert_eq!(comment.id, deserialized.id);
157        assert_eq!(comment.issue_id, deserialized.issue_id);
158        assert_eq!(comment.content, deserialized.content);
159    }
160
161    #[test]
162    fn test_comment_empty_content() {
163        let comment = Comment {
164            id: 1,
165            issue_id: 1,
166            content: "".to_string(),
167            created_at: Utc::now(),
168        };
169
170        let json = serde_json::to_string(&comment).unwrap();
171        let deserialized: Comment = serde_json::from_str(&json).unwrap();
172
173        assert_eq!(deserialized.content, "");
174    }
175
176    // ==================== Session Tests ====================
177
178    #[test]
179    fn test_session_serialization() {
180        let session = Session {
181            id: 1,
182            started_at: Utc::now(),
183            ended_at: None,
184            active_issue_id: Some(5),
185            handoff_notes: Some("Notes here".to_string()),
186            last_action: None,
187        };
188
189        let json = serde_json::to_string(&session).unwrap();
190        let deserialized: Session = serde_json::from_str(&json).unwrap();
191
192        assert_eq!(session.id, deserialized.id);
193        assert_eq!(session.active_issue_id, deserialized.active_issue_id);
194        assert_eq!(session.handoff_notes, deserialized.handoff_notes);
195    }
196
197    #[test]
198    fn test_session_ended() {
199        let now = Utc::now();
200        let session = Session {
201            id: 1,
202            started_at: now,
203            ended_at: Some(now),
204            active_issue_id: None,
205            handoff_notes: Some("Final notes".to_string()),
206            last_action: None,
207        };
208
209        let json = serde_json::to_string(&session).unwrap();
210        let deserialized: Session = serde_json::from_str(&json).unwrap();
211
212        assert_eq!(deserialized.ended_at, Some(now));
213        assert_eq!(deserialized.handoff_notes, Some("Final notes".to_string()));
214    }
215
216    // ==================== Milestone Tests ====================
217
218    #[test]
219    fn test_milestone_serialization() {
220        let milestone = Milestone {
221            id: 1,
222            name: "v1.0".to_string(),
223            description: Some("First release".to_string()),
224            status: "open".to_string(),
225            created_at: Utc::now(),
226            closed_at: None,
227        };
228
229        let json = serde_json::to_string(&milestone).unwrap();
230        let deserialized: Milestone = serde_json::from_str(&json).unwrap();
231
232        assert_eq!(milestone.id, deserialized.id);
233        assert_eq!(milestone.name, deserialized.name);
234        assert_eq!(milestone.description, deserialized.description);
235        assert_eq!(milestone.status, deserialized.status);
236    }
237
238    #[test]
239    fn test_milestone_closed() {
240        let now = Utc::now();
241        let milestone = Milestone {
242            id: 1,
243            name: "v1.0".to_string(),
244            description: None,
245            status: "closed".to_string(),
246            created_at: now,
247            closed_at: Some(now),
248        };
249
250        let json = serde_json::to_string(&milestone).unwrap();
251        let deserialized: Milestone = serde_json::from_str(&json).unwrap();
252
253        assert_eq!(deserialized.closed_at, Some(now));
254        assert_eq!(deserialized.status, "closed");
255    }
256
257    // ==================== Property-Based Tests ====================
258
259    proptest! {
260        #[test]
261        fn prop_issue_json_roundtrip(
262            id in 1i64..10000,
263            title in "[a-zA-Z0-9 ]{1,100}",
264            status in "open|closed",
265            priority in "low|medium|high|critical"
266        ) {
267            let issue = Issue {
268                id,
269                title: title.clone(),
270                description: None,
271                status: status.clone(),
272                priority: priority.clone(),
273                parent_id: None,
274                created_at: Utc::now(),
275                updated_at: Utc::now(),
276                closed_at: None,
277            };
278
279            let json = serde_json::to_string(&issue).unwrap();
280            let deserialized: Issue = serde_json::from_str(&json).unwrap();
281
282            prop_assert_eq!(deserialized.id, id);
283            prop_assert_eq!(deserialized.title, title);
284            prop_assert_eq!(deserialized.status, status);
285            prop_assert_eq!(deserialized.priority, priority);
286        }
287
288        #[test]
289        fn prop_comment_json_roundtrip(
290            id in 1i64..10000,
291            issue_id in 1i64..10000,
292            content in "[a-zA-Z0-9 ]{0,500}"
293        ) {
294            let comment = Comment {
295                id,
296                issue_id,
297                content: content.clone(),
298                created_at: Utc::now(),
299            };
300
301            let json = serde_json::to_string(&comment).unwrap();
302            let deserialized: Comment = serde_json::from_str(&json).unwrap();
303
304            prop_assert_eq!(deserialized.id, id);
305            prop_assert_eq!(deserialized.issue_id, issue_id);
306            prop_assert_eq!(deserialized.content, content);
307        }
308
309        #[test]
310        fn prop_session_json_roundtrip(
311            id in 1i64..10000,
312            active_issue_id in prop::option::of(1i64..10000),
313            handoff_notes in prop::option::of("[a-zA-Z0-9 ]{0,200}")
314        ) {
315            let session = Session {
316                id,
317                started_at: Utc::now(),
318                ended_at: None,
319                active_issue_id,
320                handoff_notes: handoff_notes.clone(),
321                last_action: None,
322            };
323
324            let json = serde_json::to_string(&session).unwrap();
325            let deserialized: Session = serde_json::from_str(&json).unwrap();
326
327            prop_assert_eq!(deserialized.id, id);
328            prop_assert_eq!(deserialized.active_issue_id, active_issue_id);
329            prop_assert_eq!(deserialized.handoff_notes, handoff_notes);
330        }
331
332        #[test]
333        fn prop_milestone_json_roundtrip(
334            id in 1i64..10000,
335            name in "[a-zA-Z0-9.]{1,50}",
336            status in "open|closed"
337        ) {
338            let milestone = Milestone {
339                id,
340                name: name.clone(),
341                description: None,
342                status: status.clone(),
343                created_at: Utc::now(),
344                closed_at: None,
345            };
346
347            let json = serde_json::to_string(&milestone).unwrap();
348            let deserialized: Milestone = serde_json::from_str(&json).unwrap();
349
350            prop_assert_eq!(deserialized.id, id);
351            prop_assert_eq!(deserialized.name, name);
352            prop_assert_eq!(deserialized.status, status);
353        }
354
355        #[test]
356        fn prop_issue_with_optional_fields(
357            has_desc in proptest::bool::ANY,
358            has_parent in proptest::bool::ANY,
359            is_closed in proptest::bool::ANY
360        ) {
361            let now = Utc::now();
362            let issue = Issue {
363                id: 1,
364                title: "Test".to_string(),
365                description: if has_desc { Some("Desc".to_string()) } else { None },
366                status: if is_closed { "closed".to_string() } else { "open".to_string() },
367                priority: "medium".to_string(),
368                parent_id: if has_parent { Some(99) } else { None },
369                created_at: now,
370                updated_at: now,
371                closed_at: if is_closed { Some(now) } else { None },
372            };
373
374            let json = serde_json::to_string(&issue).unwrap();
375            let deserialized: Issue = serde_json::from_str(&json).unwrap();
376
377            prop_assert_eq!(deserialized.description.is_some(), has_desc);
378            prop_assert_eq!(deserialized.parent_id.is_some(), has_parent);
379            prop_assert_eq!(deserialized.closed_at.is_some(), is_closed);
380        }
381    }
382}