Skip to main content

perl_workspace_index_state_machine/
lib.rs

1//! Enhanced index lifecycle state machine for production readiness.
2//!
3//! This module provides a comprehensive state machine for index lifecycle management
4//! with additional states for initialization, updating, invalidation, and error handling.
5//! The state machine ensures thread-safe state transitions with proper guards and
6//! error recovery mechanisms.
7//!
8//! # State Machine States
9//!
10//! - **Idle**: Index is idle and not initialized
11//! - **Initializing**: Index is being initialized
12//! - **Building**: Index is being built (workspace scan in progress)
13//! - **Updating**: Index is being updated (incremental changes)
14//! - **Invalidating**: Index is being invalidated
15//! - **Ready**: Index is ready for queries
16//! - **Degraded**: Index is degraded but partially functional
17//! - **Error**: Index is in error state
18//!
19//! # State Transitions
20//!
21//! ```text
22//! Idle → Initializing → Building → Updating → Ready
23//!  ↓         ↓            ↓          ↓         ↓
24//! Error ← Error ← Error ← Error ← Error ← Error
25//!  ↓         ↓            ↓          ↓         ↓
26//! Degraded ← Degraded ← Degraded ← Degraded ← Degraded
27//! ```
28//!
29//! # Thread Safety
30//!
31//! - All state transitions are protected by RwLock
32//! - State reads are lock-free (cloned state)
33//! - State writes use exclusive locks
34//! - Guards prevent invalid transitions
35//!
36//! # Usage
37//!
38//! ```rust,ignore
39//! use perl_workspace_index_state_machine::{IndexStateMachine, IndexState};
40//!
41//! let machine = IndexStateMachine::new();
42//! assert!(matches!(machine.state(), IndexState::Idle));
43//!
44//! machine.transition_to_initializing();
45//! machine.transition_to_building(100); // 100 files to index
46//! ```
47
48use parking_lot::RwLock;
49use std::sync::Arc;
50use std::time::Instant;
51
52/// Enhanced index state with additional production-ready states.
53///
54/// Extends the original IndexState with Initializing, Updating, Invalidating,
55/// and Error states for comprehensive lifecycle management.
56#[derive(Clone, Debug)]
57pub enum IndexState {
58    /// Index is idle and not initialized
59    Idle {
60        /// When the index entered idle state
61        since: Instant,
62    },
63
64    /// Index is being initialized
65    Initializing {
66        /// Initialization progress (0-100)
67        progress: u8,
68        /// When initialization started
69        started_at: Instant,
70    },
71
72    /// Index is being built (workspace scan in progress)
73    Building {
74        /// Current build phase
75        phase: BuildPhase,
76        /// Files indexed so far
77        indexed_count: usize,
78        /// Total files discovered
79        total_count: usize,
80        /// Started at
81        started_at: Instant,
82    },
83
84    /// Index is being updated (incremental changes)
85    Updating {
86        /// Number of files being updated
87        updating_count: usize,
88        /// When update started
89        started_at: Instant,
90    },
91
92    /// Index is being invalidated
93    Invalidating {
94        /// Reason for invalidation
95        reason: InvalidationReason,
96        /// When invalidation started
97        started_at: Instant,
98    },
99
100    /// Index is ready for queries
101    Ready {
102        /// Total symbols indexed
103        symbol_count: usize,
104        /// Total files indexed
105        file_count: usize,
106        /// Timestamp of last successful index
107        completed_at: Instant,
108    },
109
110    /// Index is degraded but partially functional
111    Degraded {
112        /// Why we degraded
113        reason: DegradationReason,
114        /// What's still available
115        available_symbols: usize,
116        /// When degradation occurred
117        since: Instant,
118    },
119
120    /// Index is in error state
121    Error {
122        /// Error message
123        message: String,
124        /// When error occurred
125        since: Instant,
126    },
127}
128
129impl IndexState {
130    /// Return the coarse state kind for instrumentation and routing decisions.
131    pub fn kind(&self) -> IndexStateKind {
132        match self {
133            IndexState::Idle { .. } => IndexStateKind::Idle,
134            IndexState::Initializing { .. } => IndexStateKind::Initializing,
135            IndexState::Building { .. } => IndexStateKind::Building,
136            IndexState::Updating { .. } => IndexStateKind::Updating,
137            IndexState::Invalidating { .. } => IndexStateKind::Invalidating,
138            IndexState::Ready { .. } => IndexStateKind::Ready,
139            IndexState::Degraded { .. } => IndexStateKind::Degraded,
140            IndexState::Error { .. } => IndexStateKind::Error,
141        }
142    }
143
144    /// Check if the index is ready for queries.
145    pub fn is_ready(&self) -> bool {
146        matches!(self, IndexState::Ready { .. })
147    }
148
149    /// Check if the index is in an error state.
150    pub fn is_error(&self) -> bool {
151        matches!(self, IndexState::Error { .. })
152    }
153
154    /// Check if the index is in a transitional state.
155    pub fn is_transitional(&self) -> bool {
156        matches!(
157            self,
158            IndexState::Initializing { .. }
159                | IndexState::Building { .. }
160                | IndexState::Updating { .. }
161                | IndexState::Invalidating { .. }
162        )
163    }
164
165    /// Get the timestamp of when the current state began.
166    pub fn state_started_at(&self) -> Instant {
167        match self {
168            IndexState::Idle { since } => *since,
169            IndexState::Initializing { started_at, .. } => *started_at,
170            IndexState::Building { started_at, .. } => *started_at,
171            IndexState::Updating { started_at, .. } => *started_at,
172            IndexState::Invalidating { started_at, .. } => *started_at,
173            IndexState::Ready { completed_at, .. } => *completed_at,
174            IndexState::Degraded { since, .. } => *since,
175            IndexState::Error { since, .. } => *since,
176        }
177    }
178}
179
180/// Coarse index state kinds for instrumentation and transition tracking.
181#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
182pub enum IndexStateKind {
183    /// Index is idle
184    Idle,
185    /// Index is initializing
186    Initializing,
187    /// Index is building
188    Building,
189    /// Index is updating
190    Updating,
191    /// Index is invalidating
192    Invalidating,
193    /// Index is ready
194    Ready,
195    /// Index is degraded
196    Degraded,
197    /// Index is in error state
198    Error,
199}
200
201/// Build phases for the Building state.
202#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
203pub enum BuildPhase {
204    /// No scan has started yet
205    Idle,
206    /// Workspace file discovery is in progress
207    Scanning,
208    /// Symbol indexing is in progress
209    Indexing,
210}
211
212/// Reason for index invalidation.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub enum InvalidationReason {
215    /// Workspace configuration changed
216    ConfigurationChanged,
217    /// File system watcher detected significant changes
218    FileSystemChanged,
219    /// Manual invalidation requested
220    ManualRequest,
221    /// Cache corruption detected
222    CacheCorruption,
223    /// Dependency changed
224    DependencyChanged,
225}
226
227/// Reason for index degradation.
228#[derive(Clone, Debug, PartialEq, Eq)]
229pub enum DegradationReason {
230    /// Parse storm (too many simultaneous changes)
231    ParseStorm {
232        /// Number of pending parse operations
233        pending_parses: usize,
234    },
235
236    /// IO error during indexing
237    IoError {
238        /// Error message for diagnostics
239        message: String,
240    },
241
242    /// Timeout during workspace scan
243    ScanTimeout {
244        /// Elapsed time in milliseconds
245        elapsed_ms: u64,
246    },
247
248    /// Resource limits exceeded
249    ResourceLimit {
250        /// Which resource limit was exceeded
251        kind: ResourceKind,
252    },
253}
254
255/// Type of resource limit that was exceeded.
256#[derive(Clone, Debug, PartialEq, Eq)]
257pub enum ResourceKind {
258    /// Maximum number of files in index exceeded
259    MaxFiles,
260    /// Maximum total symbols exceeded
261    MaxSymbols,
262    /// Maximum AST cache bytes exceeded
263    MaxCacheBytes,
264}
265
266/// State transition for index lifecycle instrumentation.
267#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
268pub struct IndexStateTransition {
269    /// Transition start state
270    pub from: IndexStateKind,
271    /// Transition end state
272    pub to: IndexStateKind,
273}
274
275/// A phase transition while building the workspace index.
276#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
277pub struct BuildPhaseTransition {
278    /// Transition start phase
279    pub from: BuildPhase,
280    /// Transition end phase
281    pub to: BuildPhase,
282}
283
284/// Result of a state transition attempt.
285#[derive(Clone, Debug, PartialEq, Eq)]
286pub enum TransitionResult {
287    /// Transition succeeded
288    Success,
289    /// Transition failed - invalid state transition
290    InvalidTransition {
291        /// Current state
292        from: IndexStateKind,
293        /// Target state
294        to: IndexStateKind,
295    },
296    /// Transition failed - guard condition not met
297    GuardFailed {
298        /// Guard condition that failed
299        condition: String,
300    },
301}
302
303/// Thread-safe index state machine.
304///
305/// Manages index lifecycle with comprehensive state transitions,
306/// guards, and error recovery mechanisms.
307pub struct IndexStateMachine {
308    /// Current index state (RwLock for thread-safe transitions)
309    state: Arc<RwLock<IndexState>>,
310}
311
312impl IndexStateMachine {
313    /// Create a new state machine in Idle state.
314    ///
315    /// # Returns
316    ///
317    /// A state machine initialized in `IndexState::Idle`.
318    ///
319    /// # Examples
320    ///
321    /// ```rust
322    /// use perl_workspace_index_state_machine::IndexStateMachine;
323    ///
324    /// let machine = IndexStateMachine::new();
325    /// ```
326    pub fn new() -> Self {
327        Self { state: Arc::new(RwLock::new(IndexState::Idle { since: Instant::now() })) }
328    }
329
330    /// Get current state (lock-free read via clone).
331    ///
332    /// Returns a cloned copy of the current state for lock-free access
333    /// in hot path LSP handlers.
334    ///
335    /// # Returns
336    ///
337    /// The current `IndexState` snapshot.
338    ///
339    /// # Examples
340    ///
341    /// ```rust
342    /// use perl_workspace_index_state_machine::{IndexStateMachine, IndexState};
343    ///
344    /// let machine = IndexStateMachine::new();
345    /// match machine.state() {
346    ///     IndexState::Ready { .. } => { /* Full query path */ }
347    /// _ => { /* Degraded/building fallback */ }
348    /// }
349    /// ```
350    pub fn state(&self) -> IndexState {
351        self.state.read().clone()
352    }
353
354    /// Transition to Initializing state.
355    ///
356    /// # State Transition Guards
357    ///
358    /// Only valid transitions:
359    /// - `Idle` → `Initializing`
360    /// - `Error` → `Initializing` (recovery attempt)
361    ///
362    /// # Returns
363    ///
364    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
365    ///
366    /// # Examples
367    ///
368    /// ```rust,ignore
369    /// use perl_workspace_index_state_machine::IndexStateMachine;
370    ///
371    /// let machine = IndexStateMachine::new();
372    /// assert!(matches!(machine.transition_to_initializing(), TransitionResult::Success));
373    /// ```
374    pub fn transition_to_initializing(&self) -> TransitionResult {
375        let mut state = self.state.write();
376        let from_kind = state.kind();
377
378        match &*state {
379            IndexState::Idle { .. } | IndexState::Error { .. } => {
380                *state = IndexState::Initializing { progress: 0, started_at: Instant::now() };
381                TransitionResult::Success
382            }
383            _ => TransitionResult::InvalidTransition {
384                from: from_kind,
385                to: IndexStateKind::Initializing,
386            },
387        }
388    }
389
390    /// Transition to Building state.
391    ///
392    /// # State Transition Guards
393    ///
394    /// Only valid transitions:
395    /// - `Initializing` → `Building`
396    /// - `Ready` → `Building` (re-index)
397    /// - `Degraded` → `Building` (recovery)
398    ///
399    /// # Arguments
400    ///
401    /// * `total_count` - Total number of files to index
402    ///
403    /// # Returns
404    ///
405    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
406    ///
407    /// # Examples
408    ///
409    /// ```rust,ignore
410    /// use perl_workspace_index_state_machine::IndexStateMachine;
411    ///
412    /// let machine = IndexStateMachine::new();
413    /// machine.transition_to_initializing();
414    /// assert!(matches!(machine.transition_to_building(100), TransitionResult::Success));
415    /// ```
416    pub fn transition_to_building(&self, total_count: usize) -> TransitionResult {
417        let mut state = self.state.write();
418        let from_kind = state.kind();
419
420        match &*state {
421            IndexState::Initializing { .. }
422            | IndexState::Ready { .. }
423            | IndexState::Degraded { .. } => {
424                *state = IndexState::Building {
425                    phase: BuildPhase::Idle,
426                    indexed_count: 0,
427                    total_count,
428                    started_at: Instant::now(),
429                };
430                TransitionResult::Success
431            }
432            _ => TransitionResult::InvalidTransition {
433                from: from_kind,
434                to: IndexStateKind::Building,
435            },
436        }
437    }
438
439    /// Transition to Updating state.
440    ///
441    /// # State Transition Guards
442    ///
443    /// Only valid transitions:
444    /// - `Ready` → `Updating`
445    /// - `Degraded` → `Updating`
446    ///
447    /// # Arguments
448    ///
449    /// * `updating_count` - Number of files being updated
450    ///
451    /// # Returns
452    ///
453    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
454    ///
455    /// # Examples
456    ///
457    /// ```rust,ignore
458    /// use perl_workspace_index_state_machine::IndexStateMachine;
459    ///
460    /// let machine = IndexStateMachine::new();
461    /// // ... build index ...
462    /// machine.transition_to_ready(100, 5000);
463    /// assert!(matches!(machine.transition_to_updating(5), TransitionResult::Success));
464    /// ```
465    pub fn transition_to_updating(&self, updating_count: usize) -> TransitionResult {
466        let mut state = self.state.write();
467        let from_kind = state.kind();
468
469        match &*state {
470            IndexState::Ready { .. } | IndexState::Degraded { .. } => {
471                *state = IndexState::Updating { updating_count, started_at: Instant::now() };
472                TransitionResult::Success
473            }
474            _ => TransitionResult::InvalidTransition {
475                from: from_kind,
476                to: IndexStateKind::Updating,
477            },
478        }
479    }
480
481    /// Transition to Invalidating state.
482    ///
483    /// # State Transition Guards
484    ///
485    /// Valid from any non-transitional state.
486    ///
487    /// # Arguments
488    ///
489    /// * `reason` - Reason for invalidation
490    ///
491    /// # Returns
492    ///
493    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
494    ///
495    /// # Examples
496    ///
497    /// ```rust,ignore
498    /// use perl_workspace_index_state_machine::{IndexStateMachine, InvalidationReason};
499    ///
500    /// let machine = IndexStateMachine::new();
501    /// assert!(matches!(
502    ///     machine.transition_to_invalidating(InvalidationReason::ManualRequest),
503    ///     TransitionResult::Success
504    /// ));
505    /// ```
506    pub fn transition_to_invalidating(&self, reason: InvalidationReason) -> TransitionResult {
507        let mut state = self.state.write();
508        let from_kind = state.kind();
509
510        // Can transition from any non-transitional state
511        match &*state {
512            IndexState::Initializing { .. }
513            | IndexState::Building { .. }
514            | IndexState::Updating { .. }
515            | IndexState::Invalidating { .. } => TransitionResult::InvalidTransition {
516                from: from_kind,
517                to: IndexStateKind::Invalidating,
518            },
519            _ => {
520                *state = IndexState::Invalidating { reason, started_at: Instant::now() };
521                TransitionResult::Success
522            }
523        }
524    }
525
526    /// Transition to Ready state.
527    ///
528    /// # State Transition Guards
529    ///
530    /// Only valid transitions:
531    /// - `Building` → `Ready` (normal completion)
532    /// - `Updating` → `Ready` (update complete)
533    /// - `Invalidating` → `Ready` (invalidation complete)
534    /// - `Degraded` → `Ready` (recovery after fix)
535    ///
536    /// # Arguments
537    ///
538    /// * `file_count` - Total number of files indexed
539    /// * `symbol_count` - Total number of symbols extracted
540    ///
541    /// # Returns
542    ///
543    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
544    ///
545    /// # Examples
546    ///
547    /// ```rust,ignore
548    /// use perl_workspace_index_state_machine::IndexStateMachine;
549    ///
550    /// let machine = IndexStateMachine::new();
551    /// machine.transition_to_building(100);
552    /// assert!(matches!(machine.transition_to_ready(100, 5000), TransitionResult::Success));
553    /// ```
554    pub fn transition_to_ready(&self, file_count: usize, symbol_count: usize) -> TransitionResult {
555        let mut state = self.state.write();
556        let from_kind = state.kind();
557
558        match &*state {
559            IndexState::Building { .. }
560            | IndexState::Updating { .. }
561            | IndexState::Invalidating { .. }
562            | IndexState::Degraded { .. } => {
563                *state =
564                    IndexState::Ready { symbol_count, file_count, completed_at: Instant::now() };
565                TransitionResult::Success
566            }
567            IndexState::Ready { .. } => {
568                // Already Ready - update metrics but don't log as transition
569                *state =
570                    IndexState::Ready { symbol_count, file_count, completed_at: Instant::now() };
571                TransitionResult::Success
572            }
573            _ => TransitionResult::InvalidTransition { from: from_kind, to: IndexStateKind::Ready },
574        }
575    }
576
577    /// Transition to Degraded state.
578    ///
579    /// # State Transition Guards
580    ///
581    /// Valid from any state except Error.
582    ///
583    /// # Arguments
584    ///
585    /// * `reason` - Why the index degraded
586    ///
587    /// # Returns
588    ///
589    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
590    ///
591    /// # Examples
592    ///
593    /// ```rust,ignore
594    /// use perl_workspace_index_state_machine::{IndexStateMachine, DegradationReason};
595    ///
596    /// let machine = IndexStateMachine::new();
597    /// machine.transition_to_ready(100, 5000);
598    /// assert!(matches!(
599    ///     machine.transition_to_degraded(DegradationReason::IoError {
600    ///         message: "IO error".to_string()
601    ///     }),
602    ///     TransitionResult::Success
603    /// ));
604    /// ```
605    pub fn transition_to_degraded(&self, reason: DegradationReason) -> TransitionResult {
606        let mut state = self.state.write();
607        let from_kind = state.kind();
608
609        // Get available symbols count from current state
610        let available_symbols = match &*state {
611            IndexState::Ready { symbol_count, .. } => *symbol_count,
612            IndexState::Degraded { available_symbols, .. } => *available_symbols,
613            _ => 0,
614        };
615
616        match &*state {
617            IndexState::Error { .. } => TransitionResult::InvalidTransition {
618                from: from_kind,
619                to: IndexStateKind::Degraded,
620            },
621            _ => {
622                *state = IndexState::Degraded { reason, available_symbols, since: Instant::now() };
623                TransitionResult::Success
624            }
625        }
626    }
627
628    /// Transition to Error state.
629    ///
630    /// # State Transition Guards
631    ///
632    /// Valid from any state.
633    ///
634    /// # Arguments
635    ///
636    /// * `message` - Error message
637    ///
638    /// # Returns
639    ///
640    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
641    ///
642    /// # Examples
643    ///
644    /// ```rust,ignore
645    /// use perl_workspace_index_state_machine::IndexStateMachine;
646    ///
647    /// let machine = IndexStateMachine::new();
648    /// assert!(matches!(
649    ///     machine.transition_to_error("Critical error".to_string()),
650    ///     TransitionResult::Success
651    /// ));
652    /// ```
653    pub fn transition_to_error(&self, message: String) -> TransitionResult {
654        let mut state = self.state.write();
655        let _from_kind = state.kind();
656
657        *state = IndexState::Error { message, since: Instant::now() };
658        TransitionResult::Success
659    }
660
661    /// Transition to Idle state.
662    ///
663    /// # State Transition Guards
664    ///
665    /// Valid from any state.
666    ///
667    /// # Returns
668    ///
669    /// `TransitionResult::Success` if transition succeeded, otherwise an error.
670    ///
671    /// # Examples
672    ///
673    /// ```rust,ignore
674    /// use perl_workspace_index_state_machine::IndexStateMachine;
675    ///
676    /// let machine = IndexStateMachine::new();
677    /// machine.transition_to_ready(100, 5000);
678    /// assert!(matches!(machine.transition_to_idle(), TransitionResult::Success));
679    /// ```
680    pub fn transition_to_idle(&self) -> TransitionResult {
681        let mut state = self.state.write();
682
683        *state = IndexState::Idle { since: Instant::now() };
684        TransitionResult::Success
685    }
686
687    /// Update building progress.
688    ///
689    /// # Arguments
690    ///
691    /// * `indexed_count` - Number of files indexed so far
692    /// * `phase` - Current build phase
693    ///
694    /// # Returns
695    ///
696    /// `TransitionResult::Success` if update succeeded, otherwise an error.
697    ///
698    /// # Examples
699    ///
700    /// ```rust,ignore
701    /// use perl_workspace_index_state_machine::{IndexStateMachine, BuildPhase};
702    ///
703    /// let machine = IndexStateMachine::new();
704    /// machine.transition_to_building(100);
705    /// assert!(matches!(
706    ///     machine.update_building_progress(50, BuildPhase::Indexing),
707    ///     TransitionResult::Success
708    /// ));
709    /// ```
710    pub fn update_building_progress(
711        &self,
712        indexed_count: usize,
713        phase: BuildPhase,
714    ) -> TransitionResult {
715        let mut state = self.state.write();
716
717        match &mut *state {
718            IndexState::Building { total_count, started_at, .. } => {
719                *state = IndexState::Building {
720                    phase,
721                    indexed_count,
722                    total_count: *total_count,
723                    started_at: *started_at,
724                };
725                TransitionResult::Success
726            }
727            _ => TransitionResult::InvalidTransition {
728                from: state.kind(),
729                to: IndexStateKind::Building,
730            },
731        }
732    }
733
734    /// Update initialization progress.
735    ///
736    /// # Arguments
737    ///
738    /// * `progress` - Progress percentage (0-100)
739    ///
740    /// # Returns
741    ///
742    /// `TransitionResult::Success` if update succeeded, otherwise an error.
743    ///
744    /// # Examples
745    ///
746    /// ```rust,ignore
747    /// use perl_workspace_index_state_machine::IndexStateMachine;
748    ///
749    /// let machine = IndexStateMachine::new();
750    /// machine.transition_to_initializing();
751    /// assert!(matches!(machine.update_initialization_progress(50), TransitionResult::Success));
752    /// ```
753    pub fn update_initialization_progress(&self, progress: u8) -> TransitionResult {
754        let mut state = self.state.write();
755
756        match &mut *state {
757            IndexState::Initializing { started_at, .. } => {
758                *state = IndexState::Initializing {
759                    progress: progress.min(100),
760                    started_at: *started_at,
761                };
762                TransitionResult::Success
763            }
764            _ => TransitionResult::InvalidTransition {
765                from: state.kind(),
766                to: IndexStateKind::Initializing,
767            },
768        }
769    }
770}
771
772impl Default for IndexStateMachine {
773    fn default() -> Self {
774        Self::new()
775    }
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    #[test]
783    fn test_initial_state() {
784        let machine = IndexStateMachine::new();
785        assert!(matches!(machine.state(), IndexState::Idle { .. }));
786    }
787
788    #[test]
789    fn test_idle_to_initializing() {
790        let machine = IndexStateMachine::new();
791        assert!(matches!(machine.transition_to_initializing(), TransitionResult::Success));
792        assert!(matches!(machine.state(), IndexState::Initializing { .. }));
793    }
794
795    #[test]
796    fn test_initializing_to_building() {
797        let machine = IndexStateMachine::new();
798        machine.transition_to_initializing();
799        assert!(matches!(machine.transition_to_building(100), TransitionResult::Success));
800        assert!(matches!(machine.state(), IndexState::Building { .. }));
801    }
802
803    #[test]
804    fn test_building_to_ready() {
805        let machine = IndexStateMachine::new();
806        machine.transition_to_initializing();
807        machine.transition_to_building(100);
808        assert!(matches!(machine.transition_to_ready(100, 5000), TransitionResult::Success));
809        assert!(matches!(machine.state(), IndexState::Ready { .. }));
810    }
811
812    #[test]
813    fn test_ready_to_updating() {
814        let machine = IndexStateMachine::new();
815        machine.transition_to_initializing();
816        machine.transition_to_building(100);
817        machine.transition_to_ready(100, 5000);
818        assert!(matches!(machine.transition_to_updating(5), TransitionResult::Success));
819        assert!(matches!(machine.state(), IndexState::Updating { .. }));
820    }
821
822    #[test]
823    fn test_ready_to_degraded() {
824        let machine = IndexStateMachine::new();
825        machine.transition_to_initializing();
826        machine.transition_to_building(100);
827        machine.transition_to_ready(100, 5000);
828        assert!(matches!(
829            machine.transition_to_degraded(DegradationReason::IoError {
830                message: "error".to_string()
831            }),
832            TransitionResult::Success
833        ));
834        assert!(matches!(machine.state(), IndexState::Degraded { .. }));
835    }
836
837    #[test]
838    fn test_any_to_error() {
839        let machine = IndexStateMachine::new();
840        assert!(matches!(
841            machine.transition_to_error("error".to_string()),
842            TransitionResult::Success
843        ));
844        assert!(matches!(machine.state(), IndexState::Error { .. }));
845    }
846
847    #[test]
848    fn test_invalid_transition() {
849        let machine = IndexStateMachine::new();
850        // Can't go from Idle to Ready without building
851        assert!(matches!(
852            machine.transition_to_ready(0, 0),
853            TransitionResult::InvalidTransition { .. }
854        ));
855    }
856
857    #[test]
858    fn test_update_building_progress() {
859        let machine = IndexStateMachine::new();
860        machine.transition_to_initializing();
861        machine.transition_to_building(100);
862        assert!(matches!(
863            machine.update_building_progress(50, BuildPhase::Indexing),
864            TransitionResult::Success
865        ));
866    }
867
868    #[test]
869    fn test_update_initialization_progress() {
870        let machine = IndexStateMachine::new();
871        machine.transition_to_initializing();
872        assert!(matches!(machine.update_initialization_progress(50), TransitionResult::Success));
873    }
874}