Skip to main content

flow_db/
task_sync.rs

1use flow_core::{Feature, Task};
2
3/// Convert a Feature to a Claude Code Task format.
4pub fn feature_to_task(feature: &Feature, _session_id: &str) -> Task {
5    let status = if feature.passes {
6        "completed"
7    } else if feature.in_progress {
8        "in_progress"
9    } else {
10        "pending"
11    };
12
13    let active_form = if feature.in_progress {
14        format!("Working on: {}", feature.name)
15    } else {
16        String::new()
17    };
18
19    // Convert dependency IDs to strings
20    let blocked_by: Vec<String> = feature
21        .dependencies
22        .iter()
23        .map(std::string::ToString::to_string)
24        .collect();
25
26    Task {
27        id: feature.id.to_string(),
28        subject: feature.name.clone(),
29        description: feature.description.clone(),
30        active_form,
31        status: status.to_string(),
32        owner: "flow".to_string(),
33        blocks: vec![],
34        blocked_by,
35    }
36}
37
38/// Convert a Claude Code Task to a Feature (best effort).
39/// Note: This is lossy since Tasks don't have all Feature fields.
40pub fn task_to_feature(task: &Task) -> Feature {
41    let id = task.id.parse::<i64>().unwrap_or(0);
42
43    let passes = task.status == "completed";
44    let in_progress = task.status == "in_progress";
45
46    // Parse blockedBy as dependency IDs
47    let dependencies: Vec<i64> = task
48        .blocked_by
49        .iter()
50        .filter_map(|s| s.parse::<i64>().ok())
51        .collect();
52
53    Feature {
54        id,
55        priority: 0,             // Unknown from task
56        category: String::new(), // Unknown from task
57        name: task.subject.clone(),
58        description: task.description.clone(),
59        steps: vec![],
60        passes,
61        in_progress,
62        dependencies,
63        created_at: None,
64        updated_at: None,
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_feature_to_task() {
74        let feature = Feature {
75            id: 42,
76            priority: 1,
77            category: "Backend".to_string(),
78            name: "Implement Auth".to_string(),
79            description: "Add OAuth support".to_string(),
80            steps: vec!["Step 1".to_string()],
81            passes: false,
82            in_progress: true,
83            dependencies: vec![10, 20],
84            created_at: Some("2025-01-01".to_string()),
85            updated_at: Some("2025-01-02".to_string()),
86        };
87
88        let task = feature_to_task(&feature, "session-123");
89
90        assert_eq!(task.id, "42");
91        assert_eq!(task.subject, "Implement Auth");
92        assert_eq!(task.description, "Add OAuth support");
93        assert_eq!(task.status, "in_progress");
94        assert!(task.active_form.contains("Working on"));
95        assert_eq!(task.blocked_by, vec!["10", "20"]);
96        assert_eq!(task.owner, "flow");
97    }
98
99    #[test]
100    fn test_feature_to_task_completed() {
101        let feature = Feature {
102            id: 1,
103            priority: 1,
104            category: String::new(),
105            name: "Done".to_string(),
106            description: String::new(),
107            steps: vec![],
108            passes: true,
109            in_progress: false,
110            dependencies: vec![],
111            created_at: None,
112            updated_at: None,
113        };
114
115        let task = feature_to_task(&feature, "session");
116        assert_eq!(task.status, "completed");
117        assert_eq!(task.active_form, "");
118    }
119
120    #[test]
121    fn test_feature_to_task_pending() {
122        let feature = Feature {
123            id: 2,
124            priority: 1,
125            category: String::new(),
126            name: "Pending".to_string(),
127            description: String::new(),
128            steps: vec![],
129            passes: false,
130            in_progress: false,
131            dependencies: vec![],
132            created_at: None,
133            updated_at: None,
134        };
135
136        let task = feature_to_task(&feature, "session");
137        assert_eq!(task.status, "pending");
138    }
139
140    #[test]
141    fn test_task_to_feature() {
142        let task = Task {
143            id: "123".to_string(),
144            subject: "Build API".to_string(),
145            description: "REST API".to_string(),
146            active_form: "Working...".to_string(),
147            status: "in_progress".to_string(),
148            owner: "agent".to_string(),
149            blocks: vec![],
150            blocked_by: vec!["10".to_string(), "20".to_string()],
151        };
152
153        let feature = task_to_feature(&task);
154
155        assert_eq!(feature.id, 123);
156        assert_eq!(feature.name, "Build API");
157        assert_eq!(feature.description, "REST API");
158        assert!(feature.in_progress);
159        assert!(!feature.passes);
160        assert_eq!(feature.dependencies, vec![10, 20]);
161    }
162
163    #[test]
164    fn test_task_to_feature_completed() {
165        let task = Task {
166            id: "1".to_string(),
167            subject: "Done".to_string(),
168            description: String::new(),
169            active_form: String::new(),
170            status: "completed".to_string(),
171            owner: String::new(),
172            blocks: vec![],
173            blocked_by: vec![],
174        };
175
176        let feature = task_to_feature(&task);
177        assert!(feature.passes);
178        assert!(!feature.in_progress);
179    }
180
181    #[test]
182    fn test_roundtrip_conversion() {
183        let original_feature = Feature {
184            id: 5,
185            priority: 3,
186            category: "UI".to_string(),
187            name: "Dashboard".to_string(),
188            description: "User dashboard".to_string(),
189            steps: vec![],
190            passes: false,
191            in_progress: true,
192            dependencies: vec![1, 2],
193            created_at: None,
194            updated_at: None,
195        };
196
197        let task = feature_to_task(&original_feature, "session");
198        let converted_feature = task_to_feature(&task);
199
200        // Core fields should match
201        assert_eq!(converted_feature.id, original_feature.id);
202        assert_eq!(converted_feature.name, original_feature.name);
203        assert_eq!(converted_feature.description, original_feature.description);
204        assert_eq!(converted_feature.passes, original_feature.passes);
205        assert_eq!(converted_feature.in_progress, original_feature.in_progress);
206        assert_eq!(
207            converted_feature.dependencies,
208            original_feature.dependencies
209        );
210
211        // Some fields will be lost (priority, category, steps, timestamps)
212        assert_eq!(converted_feature.priority, 0);
213        assert_eq!(converted_feature.category, "");
214    }
215}