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}