Skip to main content

flow_db/
feature.rs

1use flow_core::{CreateFeatureInput, DependencyRef, Feature, FeatureGraphNode, FeatureStats, Result};
2use rusqlite::{Connection, OptionalExtension, Row};
3use std::collections::{HashMap, HashSet};
4
5pub struct FeatureStore;
6
7impl FeatureStore {
8    /// Create a new feature.
9    pub fn create(conn: &Connection, input: &CreateFeatureInput) -> Result<Feature> {
10        let priority = input.priority.map_or_else(|| {
11                // Auto-assign priority as MAX+1
12                let max_priority: Option<i32> = conn
13                    .query_row("SELECT MAX(priority) FROM features", [], |row| row.get(0))
14                    .ok()
15                    .flatten();
16                max_priority.unwrap_or(0) + 1
17            }, |p| p);
18
19        // Validate dependencies exist
20        for dep_ref in &input.dependencies {
21            if let DependencyRef::Id(dep_id) = dep_ref {
22                let exists: bool = conn
23                    .query_row(
24                        "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
25                        [dep_id],
26                        |row| row.get(0),
27                    )
28                    .map_err(|e| {
29                        flow_core::FlowError::Database(format!("dependency check failed: {e}"))
30                    })?;
31                if !exists {
32                    return Err(flow_core::FlowError::NotFound(format!(
33                        "dependency {dep_id} not found"
34                    )));
35                }
36            }
37        }
38
39        let deps_json = serde_json::to_string(
40            &input
41                .dependencies
42                .iter()
43                .filter_map(|d| match d {
44                    DependencyRef::Id(id) => Some(*id),
45                    DependencyRef::Index { .. } => None,
46                })
47                .collect::<Vec<_>>(),
48        )?;
49        let steps_json = serde_json::to_string(&input.steps)?;
50
51        conn.execute(
52            r"
53            INSERT INTO features (name, description, priority, category, steps, dependencies)
54            VALUES (?, ?, ?, ?, ?, ?)
55            ",
56            rusqlite::params![
57                &input.name,
58                &input.description,
59                priority,
60                &input.category,
61                steps_json,
62                deps_json,
63            ],
64        )
65        .map_err(|e| flow_core::FlowError::Database(format!("insert failed: {e}")))?;
66
67        let id = conn.last_insert_rowid();
68        Self::get_by_id(conn, id)?.ok_or_else(|| {
69            flow_core::FlowError::Database("feature not found after insert".to_string())
70        })
71    }
72
73    /// Create multiple features in a single transaction, resolving index-based dependencies.
74    pub fn create_bulk(conn: &Connection, inputs: &[CreateFeatureInput]) -> Result<Vec<Feature>> {
75        if inputs.is_empty() {
76            return Ok(vec![]);
77        }
78
79        // Validate all inputs first (before starting transaction)
80        for (idx, input) in inputs.iter().enumerate() {
81            for dep_ref in &input.dependencies {
82                match dep_ref {
83                    DependencyRef::Id(dep_id) => {
84                        let exists: bool = conn
85                            .query_row(
86                                "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
87                                [dep_id],
88                                |row| row.get(0),
89                            )
90                            .map_err(|e| {
91                                flow_core::FlowError::Database(format!(
92                                    "dependency check failed: {e}"
93                                ))
94                            })?;
95                        if !exists {
96                            return Err(flow_core::FlowError::NotFound(format!(
97                                "dependency {dep_id} not found for feature at index {idx}"
98                            )));
99                        }
100                    }
101                    DependencyRef::Index { index } => {
102                        if *index >= inputs.len() {
103                            return Err(flow_core::FlowError::BadRequest(format!(
104                                "index {index} out of bounds for feature at index {idx}"
105                            )));
106                        }
107                        // Reject self-references
108                        if *index == idx {
109                            return Err(flow_core::FlowError::BadRequest(format!(
110                                "feature at index {idx} cannot depend on itself"
111                            )));
112                        }
113                    }
114                }
115            }
116        }
117
118        // Begin transaction for atomicity
119        conn.execute_batch("BEGIN IMMEDIATE")
120            .map_err(|e| flow_core::FlowError::Database(format!("begin transaction failed: {e}")))?;
121
122        let result = (|| -> Result<Vec<Feature>> {
123            // Get starting priority
124            let max_priority: Option<i32> = conn
125                .query_row("SELECT MAX(priority) FROM features", [], |row| row.get(0))
126                .ok()
127                .flatten();
128            let mut next_priority = max_priority.unwrap_or(0) + 1;
129
130            // Insert all features, tracking their IDs
131            let mut feature_ids = Vec::new();
132            for input in inputs {
133                let priority = input.priority.unwrap_or_else(|| {
134                    let p = next_priority;
135                    next_priority += 1;
136                    p
137                });
138
139                let steps_json = serde_json::to_string(&input.steps)?;
140                let deps_json = "[]"; // Temporarily empty, will update after
141
142                conn.execute(
143                    r"
144                    INSERT INTO features (name, description, priority, category, steps, dependencies)
145                    VALUES (?, ?, ?, ?, ?, ?)
146                    ",
147                    rusqlite::params![
148                        &input.name,
149                        &input.description,
150                        priority,
151                        &input.category,
152                        steps_json,
153                        deps_json,
154                    ],
155                )
156                .map_err(|e| flow_core::FlowError::Database(format!("bulk insert failed: {e}")))?;
157
158                feature_ids.push(conn.last_insert_rowid());
159            }
160
161            // Resolve index-based dependencies to real IDs and update
162            for (idx, input) in inputs.iter().enumerate() {
163                let feature_id = feature_ids[idx];
164                let mut resolved_deps = Vec::new();
165
166                for dep_ref in &input.dependencies {
167                    match dep_ref {
168                        DependencyRef::Id(id) => resolved_deps.push(*id),
169                        DependencyRef::Index { index } => resolved_deps.push(feature_ids[*index]),
170                    }
171                }
172
173                if !resolved_deps.is_empty() {
174                    let deps_json = serde_json::to_string(&resolved_deps)?;
175                    conn.execute(
176                        "UPDATE features SET dependencies = ? WHERE id = ?",
177                        rusqlite::params![deps_json, feature_id],
178                    )
179                    .map_err(|e| {
180                        flow_core::FlowError::Database(format!("dependency update failed: {e}"))
181                    })?;
182                }
183            }
184
185            // Fetch and return all created features
186            feature_ids
187                .into_iter()
188                .map(|id| {
189                    Self::get_by_id(conn, id)?
190                        .ok_or_else(|| flow_core::FlowError::Database("feature disappeared".to_string()))
191                })
192                .collect()
193        })();
194
195        match result {
196            Ok(features) => {
197                conn.execute_batch("COMMIT")
198                    .map_err(|e| flow_core::FlowError::Database(format!("commit failed: {e}")))?;
199                Ok(features)
200            }
201            Err(e) => {
202                let _ = conn.execute_batch("ROLLBACK");
203                Err(e)
204            }
205        }
206    }
207
208    /// Get a feature by ID.
209    pub fn get_by_id(conn: &Connection, id: i64) -> Result<Option<Feature>> {
210        let result = conn
211            .query_row(
212                "SELECT * FROM features WHERE id = ?",
213                [id],
214                row_to_feature,
215            )
216            .optional()
217            .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?;
218        Ok(result)
219    }
220
221    /// Get all features.
222    pub fn get_all(conn: &Connection) -> Result<Vec<Feature>> {
223        let mut stmt = conn
224            .prepare("SELECT * FROM features ORDER BY priority ASC")
225            .map_err(|e| flow_core::FlowError::Database(format!("prepare failed: {e}")))?;
226
227        let features = stmt
228            .query_map([], row_to_feature)
229            .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?
230            .collect::<std::result::Result<Vec<_>, _>>()
231            .map_err(|e| flow_core::FlowError::Database(format!("row parse failed: {e}")))?;
232
233        Ok(features)
234    }
235
236    /// Get feature statistics.
237    pub fn get_stats(conn: &Connection) -> Result<FeatureStats> {
238        let (total, passing, in_progress): (usize, usize, usize) = conn
239            .query_row(
240                r"
241                SELECT
242                    COUNT(*),
243                    SUM(CASE WHEN passes = 1 THEN 1 ELSE 0 END),
244                    SUM(CASE WHEN in_progress = 1 THEN 1 ELSE 0 END)
245                FROM features
246                ",
247                [],
248                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
249            )
250            .map_err(|e| flow_core::FlowError::Database(format!("stats query failed: {e}")))?;
251
252        let failing = total.saturating_sub(passing).saturating_sub(in_progress);
253
254        // Count blocked features (those with at least one failing dependency)
255        let all_features = Self::get_all(conn)?;
256        let blocked = all_features
257            .iter()
258            .filter(|f| {
259                f.dependencies.iter().any(|dep_id| {
260                    all_features
261                        .iter()
262                        .find(|d| d.id == *dep_id)
263                        .is_some_and(|d| !d.passes)
264                })
265            })
266            .count();
267
268        Ok(FeatureStats {
269            total,
270            passing,
271            failing,
272            in_progress,
273            blocked,
274        })
275    }
276
277    /// Get features that are ready to work on (not in progress, not passing, all deps pass).
278    pub fn get_ready(conn: &Connection) -> Result<Vec<Feature>> {
279        let all_features = Self::get_all(conn)?;
280        let passing_ids: HashSet<i64> = all_features
281            .iter()
282            .filter(|f| f.passes)
283            .map(|f| f.id)
284            .collect();
285
286        let ready: Vec<Feature> = all_features
287            .into_iter()
288            .filter(|f| {
289                !f.in_progress
290                    && !f.passes
291                    && f.dependencies.iter().all(|dep_id| passing_ids.contains(dep_id))
292            })
293            .collect();
294
295        Ok(ready)
296    }
297
298    /// Get features that are blocked by failing dependencies.
299    pub fn get_blocked(conn: &Connection) -> Result<Vec<Feature>> {
300        let all_features = Self::get_all(conn)?;
301        let passing_ids: HashSet<i64> = all_features
302            .iter()
303            .filter(|f| f.passes)
304            .map(|f| f.id)
305            .collect();
306
307        let blocked: Vec<Feature> = all_features
308            .into_iter()
309            .filter(|f| {
310                !f.dependencies.is_empty()
311                    && f.dependencies
312                        .iter()
313                        .any(|dep_id| !passing_ids.contains(dep_id))
314            })
315            .collect();
316
317        Ok(blocked)
318    }
319
320    /// Atomically claim a feature and mark it in progress.
321    pub fn claim_and_get(conn: &Connection, id: i64) -> Result<Feature> {
322        let rows_affected = conn
323            .execute(
324                r"
325                UPDATE features
326                SET in_progress = 1, updated_at = datetime('now')
327                WHERE id = ? AND in_progress = 0 AND passes = 0
328                ",
329                [id],
330            )
331            .map_err(|e| flow_core::FlowError::Database(format!("claim failed: {e}")))?;
332
333        if rows_affected == 0 {
334            return Err(flow_core::FlowError::Conflict(format!(
335                "feature {id} is already claimed or completed"
336            )));
337        }
338
339        Self::get_by_id(conn, id)?
340            .ok_or_else(|| flow_core::FlowError::NotFound(format!("feature {id} not found")))
341    }
342
343    /// Mark a feature as passing.
344    pub fn mark_passing(conn: &Connection, id: i64) -> Result<()> {
345        let rows = conn
346            .execute(
347                "UPDATE features SET passes = 1, in_progress = 0, updated_at = datetime('now') WHERE id = ?",
348                [id],
349            )
350            .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
351        if rows == 0 {
352            return Err(flow_core::FlowError::NotFound(format!(
353                "feature {id} not found"
354            )));
355        }
356        Ok(())
357    }
358
359    /// Mark a feature as failing.
360    pub fn mark_failing(conn: &Connection, id: i64) -> Result<()> {
361        let rows = conn
362            .execute(
363                "UPDATE features SET passes = 0, in_progress = 0, updated_at = datetime('now') WHERE id = ?",
364                [id],
365            )
366            .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
367        if rows == 0 {
368            return Err(flow_core::FlowError::NotFound(format!(
369                "feature {id} not found"
370            )));
371        }
372        Ok(())
373    }
374
375    /// Mark a feature as in progress (atomic).
376    pub fn mark_in_progress(conn: &Connection, id: i64) -> Result<()> {
377        let rows = conn
378            .execute(
379                "UPDATE features SET in_progress = 1, updated_at = datetime('now') WHERE id = ? AND in_progress = 0",
380                [id],
381            )
382            .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
383
384        if rows == 0 {
385            return Err(flow_core::FlowError::Conflict(format!(
386                "feature {id} is already in progress"
387            )));
388        }
389        Ok(())
390    }
391
392    /// Clear in-progress flag.
393    pub fn clear_in_progress(conn: &Connection, id: i64) -> Result<()> {
394        conn.execute(
395            "UPDATE features SET in_progress = 0, updated_at = datetime('now') WHERE id = ?",
396            [id],
397        )
398        .map_err(|e| flow_core::FlowError::Database(format!("update failed: {e}")))?;
399        Ok(())
400    }
401
402    /// Skip a feature by moving it to the end of the priority queue.
403    pub fn skip(conn: &Connection, id: i64) -> Result<()> {
404        let max_priority: Option<i32> = conn
405            .query_row("SELECT MAX(priority) FROM features", [], |row| row.get(0))
406            .ok()
407            .flatten();
408        let new_priority = max_priority.unwrap_or(0) + 1;
409
410        conn.execute(
411            "UPDATE features SET priority = ?, updated_at = datetime('now') WHERE id = ?",
412            rusqlite::params![new_priority, id],
413        )
414        .map_err(|e| flow_core::FlowError::Database(format!("skip failed: {e}")))?;
415        Ok(())
416    }
417
418    /// Add a dependency to a feature.
419    /// Uses SAVEPOINT for atomic read-modify-write to prevent TOCTOU races.
420    pub fn add_dependency(conn: &Connection, feature_id: i64, dep_id: i64) -> Result<()> {
421        conn.execute_batch("SAVEPOINT add_dep")
422            .map_err(|e| flow_core::FlowError::Database(format!("savepoint failed: {e}")))?;
423
424        let result = (|| -> Result<()> {
425            // Verify dependency exists
426            let exists: bool = conn
427                .query_row(
428                    "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
429                    [dep_id],
430                    |row| row.get(0),
431                )
432                .map_err(|e| flow_core::FlowError::Database(format!("exists check failed: {e}")))?;
433
434            if !exists {
435                return Err(flow_core::FlowError::NotFound(format!(
436                    "dependency {dep_id} not found"
437                )));
438            }
439
440            // Get current dependencies (within the savepoint for consistency)
441            let deps_json: String = conn
442                .query_row(
443                    "SELECT dependencies FROM features WHERE id = ?",
444                    [feature_id],
445                    |row| row.get(0),
446                )
447                .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?;
448
449            let mut deps: Vec<i64> = serde_json::from_str(&deps_json)?;
450
451            if !deps.contains(&dep_id) {
452                deps.push(dep_id);
453                let new_deps_json = serde_json::to_string(&deps)?;
454                conn.execute(
455                    "UPDATE features SET dependencies = ?, updated_at = datetime('now') WHERE id = ?",
456                    rusqlite::params![new_deps_json, feature_id],
457                )
458                .map_err(|e| {
459                    flow_core::FlowError::Database(format!("dependency add failed: {e}"))
460                })?;
461            }
462
463            Ok(())
464        })();
465
466        match result {
467            Ok(()) => {
468                conn.execute_batch("RELEASE SAVEPOINT add_dep")
469                    .map_err(|e| flow_core::FlowError::Database(format!("release failed: {e}")))?;
470                Ok(())
471            }
472            Err(e) => {
473                let _ = conn.execute_batch("ROLLBACK TO SAVEPOINT add_dep");
474                Err(e)
475            }
476        }
477    }
478
479    /// Remove a dependency from a feature.
480    /// Uses SAVEPOINT for atomic read-modify-write to prevent TOCTOU races.
481    pub fn remove_dependency(conn: &Connection, feature_id: i64, dep_id: i64) -> Result<()> {
482        conn.execute_batch("SAVEPOINT rm_dep")
483            .map_err(|e| flow_core::FlowError::Database(format!("savepoint failed: {e}")))?;
484
485        let result = (|| -> Result<()> {
486            let deps_json: String = conn
487                .query_row(
488                    "SELECT dependencies FROM features WHERE id = ?",
489                    [feature_id],
490                    |row| row.get(0),
491                )
492                .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?;
493
494            let mut deps: Vec<i64> = serde_json::from_str(&deps_json)?;
495            deps.retain(|&id| id != dep_id);
496
497            let new_deps_json = serde_json::to_string(&deps)?;
498            conn.execute(
499                "UPDATE features SET dependencies = ?, updated_at = datetime('now') WHERE id = ?",
500                rusqlite::params![new_deps_json, feature_id],
501            )
502            .map_err(|e| {
503                flow_core::FlowError::Database(format!("dependency remove failed: {e}"))
504            })?;
505
506            Ok(())
507        })();
508
509        match result {
510            Ok(()) => {
511                conn.execute_batch("RELEASE SAVEPOINT rm_dep")
512                    .map_err(|e| flow_core::FlowError::Database(format!("release failed: {e}")))?;
513                Ok(())
514            }
515            Err(e) => {
516                let _ = conn.execute_batch("ROLLBACK TO SAVEPOINT rm_dep");
517                Err(e)
518            }
519        }
520    }
521
522    /// Set all dependencies for a feature (replaces existing).
523    /// Uses SAVEPOINT for atomic validation + update.
524    pub fn set_dependencies(conn: &Connection, feature_id: i64, dep_ids: &[i64]) -> Result<()> {
525        conn.execute_batch("SAVEPOINT set_deps")
526            .map_err(|e| flow_core::FlowError::Database(format!("savepoint failed: {e}")))?;
527
528        let result = (|| -> Result<()> {
529            // Verify all dependencies exist
530            for dep_id in dep_ids {
531                let exists: bool = conn
532                    .query_row(
533                        "SELECT EXISTS(SELECT 1 FROM features WHERE id = ?)",
534                        [dep_id],
535                        |row| row.get(0),
536                    )
537                    .map_err(|e| {
538                        flow_core::FlowError::Database(format!("exists check failed: {e}"))
539                    })?;
540
541                if !exists {
542                    return Err(flow_core::FlowError::NotFound(format!(
543                        "dependency {dep_id} not found"
544                    )));
545                }
546            }
547
548            let deps_json = serde_json::to_string(dep_ids)?;
549            conn.execute(
550                "UPDATE features SET dependencies = ?, updated_at = datetime('now') WHERE id = ?",
551                rusqlite::params![deps_json, feature_id],
552            )
553            .map_err(|e| flow_core::FlowError::Database(format!("set dependencies failed: {e}")))?;
554
555            Ok(())
556        })();
557
558        match result {
559            Ok(()) => {
560                conn.execute_batch("RELEASE SAVEPOINT set_deps")
561                    .map_err(|e| flow_core::FlowError::Database(format!("release failed: {e}")))?;
562                Ok(())
563            }
564            Err(e) => {
565                let _ = conn.execute_batch("ROLLBACK TO SAVEPOINT set_deps");
566                Err(e)
567            }
568        }
569    }
570
571    /// Get the complete dependency graph with dependents computed.
572    pub fn get_graph(conn: &Connection) -> Result<Vec<FeatureGraphNode>> {
573        let features = Self::get_all(conn)?;
574        let passing_ids: HashSet<i64> = features
575            .iter()
576            .filter(|f| f.passes)
577            .map(|f| f.id)
578            .collect();
579
580        // Build reverse dependency map (dependents)
581        let mut dependents_map: HashMap<i64, Vec<i64>> = HashMap::new();
582        for feature in &features {
583            for dep_id in &feature.dependencies {
584                dependents_map
585                    .entry(*dep_id)
586                    .or_default()
587                    .push(feature.id);
588            }
589        }
590
591        let graph: Vec<FeatureGraphNode> = features
592            .into_iter()
593            .map(|f| {
594                let blocked = !f.dependencies.is_empty()
595                    && f.dependencies
596                        .iter()
597                        .any(|dep_id| !passing_ids.contains(dep_id));
598
599                FeatureGraphNode {
600                    id: f.id,
601                    name: f.name,
602                    category: f.category,
603                    priority: f.priority,
604                    passes: f.passes,
605                    in_progress: f.in_progress,
606                    blocked,
607                    dependencies: f.dependencies,
608                    dependents: dependents_map.get(&f.id).cloned().unwrap_or_default(),
609                }
610            })
611            .collect();
612
613        Ok(graph)
614    }
615}
616
617/// Helper to convert a database row to a Feature.
618fn row_to_feature(row: &Row) -> rusqlite::Result<Feature> {
619    let steps_json: String = row.get("steps")?;
620    let steps: Vec<String> = serde_json::from_str(&steps_json).map_err(|e| {
621        rusqlite::Error::FromSqlConversionFailure(
622            0,
623            rusqlite::types::Type::Text,
624            Box::new(e),
625        )
626    })?;
627
628    let deps_json: String = row.get("dependencies")?;
629    let dependencies: Vec<i64> = serde_json::from_str(&deps_json).map_err(|e| {
630        rusqlite::Error::FromSqlConversionFailure(
631            0,
632            rusqlite::types::Type::Text,
633            Box::new(e),
634        )
635    })?;
636
637    let passes_int: i32 = row.get("passes")?;
638    let in_progress_int: i32 = row.get("in_progress")?;
639
640    Ok(Feature {
641        id: row.get("id")?,
642        priority: row.get("priority")?,
643        category: row.get("category")?,
644        name: row.get("name")?,
645        description: row.get("description")?,
646        steps,
647        passes: passes_int != 0,
648        in_progress: in_progress_int != 0,
649        dependencies,
650        created_at: row.get("created_at")?,
651        updated_at: row.get("updated_at")?,
652    })
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658    use crate::Database;
659
660    #[test]
661    fn test_create_and_get() {
662        let db = Database::open_in_memory().unwrap();
663        let conn = db.writer().lock().unwrap();
664
665        let feature = FeatureStore::create(
666            &conn,
667            &CreateFeatureInput {
668                name: "Test Feature".to_string(),
669                description: "Test Description".to_string(),
670                priority: Some(1),
671                category: "Test".to_string(),
672                steps: vec!["Step 1".to_string(), "Step 2".to_string()],
673                dependencies: vec![],
674            },
675        )
676        .unwrap();
677
678        assert_eq!(feature.id, 1);
679        assert_eq!(feature.name, "Test Feature");
680        assert_eq!(feature.priority, 1);
681        assert_eq!(feature.steps.len(), 2);
682
683        let retrieved = FeatureStore::get_by_id(&conn, 1).unwrap().unwrap();
684        assert_eq!(retrieved.name, "Test Feature");
685    }
686
687    #[test]
688    fn test_bulk_create_with_index_dependencies() {
689        let db = Database::open_in_memory().unwrap();
690        let conn = db.writer().lock().unwrap();
691
692        let inputs = vec![
693            CreateFeatureInput {
694                name: "Feature A".to_string(),
695                description: "".to_string(),
696                priority: None,
697                category: "".to_string(),
698                steps: vec![],
699                dependencies: vec![],
700            },
701            CreateFeatureInput {
702                name: "Feature B".to_string(),
703                description: "".to_string(),
704                priority: None,
705                category: "".to_string(),
706                steps: vec![],
707                dependencies: vec![DependencyRef::Index { index: 0 }],
708            },
709            CreateFeatureInput {
710                name: "Feature C".to_string(),
711                description: "".to_string(),
712                priority: None,
713                category: "".to_string(),
714                steps: vec![],
715                dependencies: vec![DependencyRef::Index { index: 0 }, DependencyRef::Index { index: 1 }],
716            },
717        ];
718
719        let features = FeatureStore::create_bulk(&conn, &inputs).unwrap();
720        assert_eq!(features.len(), 3);
721
722        // Feature B should depend on Feature A
723        assert_eq!(features[1].dependencies, vec![features[0].id]);
724
725        // Feature C should depend on both A and B
726        assert_eq!(
727            features[2].dependencies,
728            vec![features[0].id, features[1].id]
729        );
730    }
731
732    #[test]
733    fn test_claim_and_get() {
734        let db = Database::open_in_memory().unwrap();
735        let conn = db.writer().lock().unwrap();
736
737        let feature = FeatureStore::create(
738            &conn,
739            &CreateFeatureInput {
740                name: "Claimable".to_string(),
741                description: "".to_string(),
742                priority: Some(1),
743                category: "".to_string(),
744                steps: vec![],
745                dependencies: vec![],
746            },
747        )
748        .unwrap();
749
750        // First claim should succeed
751        let claimed = FeatureStore::claim_and_get(&conn, feature.id).unwrap();
752        assert!(claimed.in_progress);
753
754        // Second claim should fail
755        let result = FeatureStore::claim_and_get(&conn, feature.id);
756        assert!(result.is_err());
757        assert!(matches!(result.unwrap_err(), flow_core::FlowError::Conflict(_)));
758    }
759
760    #[test]
761    fn test_mark_passing_failing() {
762        let db = Database::open_in_memory().unwrap();
763        let conn = db.writer().lock().unwrap();
764
765        let feature = FeatureStore::create(
766            &conn,
767            &CreateFeatureInput {
768                name: "Test".to_string(),
769                description: "".to_string(),
770                priority: Some(1),
771                category: "".to_string(),
772                steps: vec![],
773                dependencies: vec![],
774            },
775        )
776        .unwrap();
777
778        // Mark passing
779        FeatureStore::mark_passing(&conn, feature.id).unwrap();
780        let updated = FeatureStore::get_by_id(&conn, feature.id).unwrap().unwrap();
781        assert!(updated.passes);
782        assert!(!updated.in_progress);
783
784        // Mark failing
785        FeatureStore::mark_failing(&conn, feature.id).unwrap();
786        let updated = FeatureStore::get_by_id(&conn, feature.id).unwrap().unwrap();
787        assert!(!updated.passes);
788        assert!(!updated.in_progress);
789    }
790
791    #[test]
792    fn test_dependencies() {
793        let db = Database::open_in_memory().unwrap();
794        let conn = db.writer().lock().unwrap();
795
796        let f1 = FeatureStore::create(
797            &conn,
798            &CreateFeatureInput {
799                name: "F1".to_string(),
800                description: "".to_string(),
801                priority: Some(1),
802                category: "".to_string(),
803                steps: vec![],
804                dependencies: vec![],
805            },
806        )
807        .unwrap();
808
809        let f2 = FeatureStore::create(
810            &conn,
811            &CreateFeatureInput {
812                name: "F2".to_string(),
813                description: "".to_string(),
814                priority: Some(2),
815                category: "".to_string(),
816                steps: vec![],
817                dependencies: vec![],
818            },
819        )
820        .unwrap();
821
822        // Add dependency
823        FeatureStore::add_dependency(&conn, f2.id, f1.id).unwrap();
824        let updated = FeatureStore::get_by_id(&conn, f2.id).unwrap().unwrap();
825        assert_eq!(updated.dependencies, vec![f1.id]);
826
827        // Remove dependency
828        FeatureStore::remove_dependency(&conn, f2.id, f1.id).unwrap();
829        let updated = FeatureStore::get_by_id(&conn, f2.id).unwrap().unwrap();
830        assert!(updated.dependencies.is_empty());
831
832        // Set dependencies
833        FeatureStore::set_dependencies(&conn, f2.id, &[f1.id]).unwrap();
834        let updated = FeatureStore::get_by_id(&conn, f2.id).unwrap().unwrap();
835        assert_eq!(updated.dependencies, vec![f1.id]);
836    }
837
838    #[test]
839    fn test_get_ready_and_blocked() {
840        let db = Database::open_in_memory().unwrap();
841        let conn = db.writer().lock().unwrap();
842
843        let f1 = FeatureStore::create(
844            &conn,
845            &CreateFeatureInput {
846                name: "F1".to_string(),
847                description: "".to_string(),
848                priority: Some(1),
849                category: "".to_string(),
850                steps: vec![],
851                dependencies: vec![],
852            },
853        )
854        .unwrap();
855
856        let f2 = FeatureStore::create(
857            &conn,
858            &CreateFeatureInput {
859                name: "F2".to_string(),
860                description: "".to_string(),
861                priority: Some(2),
862                category: "".to_string(),
863                steps: vec![],
864                dependencies: vec![DependencyRef::Id(f1.id)],
865            },
866        )
867        .unwrap();
868
869        // F1 should be ready (no deps), F2 should be blocked (F1 not passing)
870        let ready = FeatureStore::get_ready(&conn).unwrap();
871        assert_eq!(ready.len(), 1);
872        assert_eq!(ready[0].id, f1.id);
873
874        let blocked = FeatureStore::get_blocked(&conn).unwrap();
875        assert_eq!(blocked.len(), 1);
876        assert_eq!(blocked[0].id, f2.id);
877
878        // Mark F1 as passing
879        FeatureStore::mark_passing(&conn, f1.id).unwrap();
880
881        // Now F2 should be ready
882        let ready = FeatureStore::get_ready(&conn).unwrap();
883        assert_eq!(ready.len(), 1);
884        assert_eq!(ready[0].id, f2.id);
885
886        let blocked = FeatureStore::get_blocked(&conn).unwrap();
887        assert_eq!(blocked.len(), 0);
888    }
889
890    #[test]
891    fn test_get_stats() {
892        let db = Database::open_in_memory().unwrap();
893        let conn = db.writer().lock().unwrap();
894
895        let f1 = FeatureStore::create(
896            &conn,
897            &CreateFeatureInput {
898                name: "F1".to_string(),
899                description: "".to_string(),
900                priority: Some(1),
901                category: "".to_string(),
902                steps: vec![],
903                dependencies: vec![],
904            },
905        )
906        .unwrap();
907
908        let f2 = FeatureStore::create(
909            &conn,
910            &CreateFeatureInput {
911                name: "F2".to_string(),
912                description: "".to_string(),
913                priority: Some(2),
914                category: "".to_string(),
915                steps: vec![],
916                dependencies: vec![],
917            },
918        )
919        .unwrap();
920
921        FeatureStore::mark_passing(&conn, f1.id).unwrap();
922        FeatureStore::mark_in_progress(&conn, f2.id).unwrap();
923
924        let stats = FeatureStore::get_stats(&conn).unwrap();
925        assert_eq!(stats.total, 2);
926        assert_eq!(stats.passing, 1);
927        assert_eq!(stats.in_progress, 1);
928        assert_eq!(stats.failing, 0);
929    }
930
931    #[test]
932    fn test_get_graph() {
933        let db = Database::open_in_memory().unwrap();
934        let conn = db.writer().lock().unwrap();
935
936        let f1 = FeatureStore::create(
937            &conn,
938            &CreateFeatureInput {
939                name: "F1".to_string(),
940                description: "".to_string(),
941                priority: Some(1),
942                category: "Cat1".to_string(),
943                steps: vec![],
944                dependencies: vec![],
945            },
946        )
947        .unwrap();
948
949        let f2 = FeatureStore::create(
950            &conn,
951            &CreateFeatureInput {
952                name: "F2".to_string(),
953                description: "".to_string(),
954                priority: Some(2),
955                category: "Cat2".to_string(),
956                steps: vec![],
957                dependencies: vec![DependencyRef::Id(f1.id)],
958            },
959        )
960        .unwrap();
961
962        let graph = FeatureStore::get_graph(&conn).unwrap();
963        assert_eq!(graph.len(), 2);
964
965        // F1 should have F2 as a dependent
966        let f1_node = graph.iter().find(|n| n.id == f1.id).unwrap();
967        assert_eq!(f1_node.dependents, vec![f2.id]);
968
969        // F2 should be blocked (F1 not passing)
970        let f2_node = graph.iter().find(|n| n.id == f2.id).unwrap();
971        assert!(f2_node.blocked);
972        assert_eq!(f2_node.dependencies, vec![f1.id]);
973    }
974}