mcp_langbase_reasoning/modes/
backtracking.rs

1//! Backtracking reasoning mode - restore from checkpoints and explore alternative paths
2
3use serde::{Deserialize, Serialize};
4use std::time::Instant;
5use tracing::{debug, info};
6
7use super::ModeCore;
8use crate::config::Config;
9use crate::error::{AppResult, ToolError};
10use crate::langbase::{LangbaseClient, Message, PipeRequest};
11use crate::prompts::BACKTRACKING_PROMPT;
12use crate::storage::{Checkpoint, SnapshotType, SqliteStorage, StateSnapshot, Storage, Thought};
13
14/// Input parameters for backtracking
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct BacktrackingParams {
17    /// Checkpoint ID to restore from
18    pub checkpoint_id: String,
19    /// New direction or approach to try
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub new_direction: Option<String>,
22    /// Optional session ID
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub session_id: Option<String>,
25    /// Confidence threshold
26    #[serde(default = "default_confidence")]
27    pub confidence: f64,
28}
29
30fn default_confidence() -> f64 {
31    0.8
32}
33
34/// Result of backtracking operation.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct BacktrackingResult {
37    /// The ID of the new thought created after backtracking.
38    pub thought_id: String,
39    /// The session ID.
40    pub session_id: String,
41    /// The ID of the checkpoint that was restored.
42    pub checkpoint_restored: String,
43    /// The new thought content generated after restoration.
44    pub content: String,
45    /// Confidence in the new thought (0.0-1.0).
46    pub confidence: f64,
47    /// Optional new branch ID if branching from checkpoint.
48    pub new_branch_id: Option<String>,
49    /// The ID of the state snapshot created for this backtrack.
50    pub snapshot_id: String,
51}
52
53/// Langbase response for backtracking
54#[allow(dead_code)] // Fields needed for deserialization but not all are used directly
55#[derive(Debug, Clone, Deserialize)]
56struct BacktrackingResponse {
57    thought: String,
58    confidence: f64,
59    #[serde(default)]
60    context_restored: bool,
61    #[serde(default)]
62    branch_from: Option<String>,
63    #[serde(default)]
64    new_direction: Option<String>,
65    #[serde(default)]
66    metadata: Option<serde_json::Value>,
67}
68
69impl BacktrackingResponse {
70    /// Parse completion - returns error on parse failure (no fallbacks).
71    fn from_completion(completion: &str) -> Result<Self, ToolError> {
72        serde_json::from_str::<BacktrackingResponse>(completion).map_err(|e| {
73            let preview: String = completion.chars().take(200).collect();
74            ToolError::ParseFailed {
75                mode: "backtracking".to_string(),
76                message: format!("JSON parse error: {} | Response preview: {}", e, preview),
77            }
78        })
79    }
80}
81
82/// Backtracking mode handler for checkpoint-based exploration.
83#[derive(Clone)]
84pub struct BacktrackingMode {
85    /// Core infrastructure (storage and langbase client).
86    core: ModeCore,
87    /// The Langbase pipe name for backtracking.
88    pipe_name: String,
89}
90
91impl BacktrackingMode {
92    /// Create a new backtracking mode handler
93    pub fn new(storage: SqliteStorage, langbase: LangbaseClient, config: &Config) -> Self {
94        Self {
95            core: ModeCore::new(storage, langbase),
96            pipe_name: config
97                .pipes
98                .backtracking
99                .clone()
100                .unwrap_or_else(|| "backtracking-reasoning-v1".to_string()),
101        }
102    }
103
104    /// Process a backtracking request
105    pub async fn process(&self, params: BacktrackingParams) -> AppResult<BacktrackingResult> {
106        let start = Instant::now();
107
108        // Get the checkpoint
109        let checkpoint = self
110            .core
111            .storage()
112            .get_checkpoint(&params.checkpoint_id)
113            .await?
114            .ok_or_else(|| ToolError::Validation {
115                field: "checkpoint_id".to_string(),
116                reason: format!("Checkpoint not found: {}", params.checkpoint_id),
117            })?;
118
119        debug!(checkpoint_id = %checkpoint.id, "Restoring from checkpoint");
120
121        // Get or verify session
122        let session =
123            match &params.session_id {
124                Some(id) => {
125                    if id != &checkpoint.session_id {
126                        return Err(ToolError::Validation {
127                            field: "session_id".to_string(),
128                            reason: "Session ID does not match checkpoint".to_string(),
129                        }
130                        .into());
131                    }
132                    self.core.storage().get_session(id).await?.ok_or_else(|| {
133                        ToolError::Validation {
134                            field: "session_id".to_string(),
135                            reason: format!("Session not found: {}", id),
136                        }
137                    })?
138                }
139                None => self
140                    .core
141                    .storage()
142                    .get_session(&checkpoint.session_id)
143                    .await?
144                    .ok_or_else(|| ToolError::Validation {
145                        field: "checkpoint_id".to_string(),
146                        reason: format!(
147                            "Session for checkpoint not found: {}",
148                            checkpoint.session_id
149                        ),
150                    })?,
151            };
152
153        // Create a state snapshot before backtracking
154        let snapshot = StateSnapshot::new(&session.id, checkpoint.snapshot.clone())
155            .with_type(SnapshotType::Branch)
156            .with_description(format!("Backtrack from checkpoint: {}", checkpoint.name));
157
158        self.core.storage().create_snapshot(&snapshot).await?;
159
160        // Build context for Langbase
161        let messages = self.build_messages(&checkpoint, params.new_direction.as_deref());
162
163        // Call Langbase pipe
164        let request = PipeRequest::new(&self.pipe_name, messages);
165        let response = self.core.langbase().call_pipe(request).await?;
166
167        // Parse response
168        let backtrack_response = BacktrackingResponse::from_completion(&response.completion)?;
169
170        // Create the new thought
171        let thought = Thought::new(&session.id, &backtrack_response.thought, "backtracking")
172            .with_confidence(backtrack_response.confidence.max(params.confidence));
173
174        self.core.storage().create_thought(&thought).await?;
175
176        let latency = start.elapsed().as_millis() as i64;
177        info!(
178            session_id = %session.id,
179            thought_id = %thought.id,
180            checkpoint_id = %checkpoint.id,
181            latency_ms = latency,
182            "Backtracking completed"
183        );
184
185        Ok(BacktrackingResult {
186            thought_id: thought.id,
187            session_id: session.id,
188            checkpoint_restored: checkpoint.id,
189            content: backtrack_response.thought,
190            confidence: backtrack_response.confidence,
191            new_branch_id: checkpoint.branch_id,
192            snapshot_id: snapshot.id,
193        })
194    }
195
196    /// Build messages for the Langbase pipe
197    fn build_messages(&self, checkpoint: &Checkpoint, new_direction: Option<&str>) -> Vec<Message> {
198        let mut messages = Vec::new();
199
200        messages.push(Message::system(BACKTRACKING_PROMPT));
201
202        // Add checkpoint context
203        let checkpoint_context = format!(
204            "Restoring from checkpoint: {}\n\nCheckpoint state:\n{}\n\n{}",
205            checkpoint.name,
206            serde_json::to_string_pretty(&checkpoint.snapshot).unwrap_or_default(),
207            checkpoint
208                .description
209                .as_ref()
210                .map(|d| format!("Description: {}", d))
211                .unwrap_or_default()
212        );
213
214        messages.push(Message::user(checkpoint_context));
215
216        // Add new direction if provided
217        if let Some(direction) = new_direction {
218            messages.push(Message::user(format!(
219                "New direction to explore: {}",
220                direction
221            )));
222        } else {
223            messages.push(Message::user(
224                "Please continue reasoning from this checkpoint, exploring an alternative approach."
225                    .to_string(),
226            ));
227        }
228
229        messages
230    }
231
232    /// Create a checkpoint at the current state
233    pub async fn create_checkpoint(
234        &self,
235        session_id: &str,
236        name: &str,
237        description: Option<&str>,
238    ) -> AppResult<Checkpoint> {
239        // Get current session state
240        let thoughts = self.core.storage().get_session_thoughts(session_id).await?;
241        let branches = self.core.storage().get_session_branches(session_id).await?;
242
243        // Serialize state
244        let state = serde_json::json!({
245            "thoughts": thoughts,
246            "branches": branches,
247            "created_at": chrono::Utc::now().to_rfc3339(),
248        });
249
250        let mut checkpoint = Checkpoint::new(session_id, name, state);
251        if let Some(desc) = description {
252            checkpoint = checkpoint.with_description(desc);
253        }
254
255        self.core.storage().create_checkpoint(&checkpoint).await?;
256
257        info!(
258            session_id = %session_id,
259            checkpoint_id = %checkpoint.id,
260            "Checkpoint created"
261        );
262
263        Ok(checkpoint)
264    }
265
266    /// List available checkpoints for a session
267    pub async fn list_checkpoints(&self, session_id: &str) -> AppResult<Vec<Checkpoint>> {
268        Ok(self
269            .core
270            .storage()
271            .get_session_checkpoints(session_id)
272            .await?)
273    }
274}
275
276impl BacktrackingParams {
277    /// Create new params with checkpoint ID
278    pub fn new(checkpoint_id: impl Into<String>) -> Self {
279        Self {
280            checkpoint_id: checkpoint_id.into(),
281            new_direction: None,
282            session_id: None,
283            confidence: default_confidence(),
284        }
285    }
286
287    /// Set new direction
288    pub fn with_direction(mut self, direction: impl Into<String>) -> Self {
289        self.new_direction = Some(direction.into());
290        self
291    }
292
293    /// Set session ID
294    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
295        self.session_id = Some(session_id.into());
296        self
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    // ============================================================================
305    // Helper Functions
306    // ============================================================================
307
308    fn create_test_config() -> Config {
309        use crate::config::{
310            DatabaseConfig, ErrorHandlingConfig, LangbaseConfig, LogFormat, LoggingConfig,
311            PipeConfig,
312        };
313        use std::path::PathBuf;
314
315        Config {
316            langbase: LangbaseConfig {
317                api_key: "test-key".to_string(),
318                base_url: "https://api.langbase.com".to_string(),
319            },
320            database: DatabaseConfig {
321                path: PathBuf::from(":memory:"),
322                max_connections: 5,
323            },
324            logging: LoggingConfig {
325                level: "info".to_string(),
326                format: LogFormat::Pretty,
327            },
328            request: crate::config::RequestConfig::default(),
329            pipes: PipeConfig::default(),
330            error_handling: ErrorHandlingConfig::default(),
331        }
332    }
333
334    // ============================================================================
335    // BacktrackingParams Tests
336    // ============================================================================
337
338    #[test]
339    fn test_backtracking_params_new() {
340        let params = BacktrackingParams::new("checkpoint-123");
341        assert_eq!(params.checkpoint_id, "checkpoint-123");
342        assert!(params.new_direction.is_none());
343        assert!(params.session_id.is_none());
344        assert_eq!(params.confidence, 0.8);
345    }
346
347    #[test]
348    fn test_backtracking_params_with_direction() {
349        let params = BacktrackingParams::new("cp-1").with_direction("Try a different approach");
350        assert_eq!(
351            params.new_direction,
352            Some("Try a different approach".to_string())
353        );
354    }
355
356    #[test]
357    fn test_backtracking_params_with_session() {
358        let params = BacktrackingParams::new("cp-1").with_session("sess-123");
359        assert_eq!(params.session_id, Some("sess-123".to_string()));
360    }
361
362    #[test]
363    fn test_backtracking_params_builder_chain() {
364        let params = BacktrackingParams::new("cp-abc")
365            .with_direction("Alternative path")
366            .with_session("session-xyz");
367
368        assert_eq!(params.checkpoint_id, "cp-abc");
369        assert_eq!(params.new_direction, Some("Alternative path".to_string()));
370        assert_eq!(params.session_id, Some("session-xyz".to_string()));
371        assert_eq!(params.confidence, 0.8);
372    }
373
374    #[test]
375    fn test_backtracking_params_serialize() {
376        let params = BacktrackingParams::new("cp-1")
377            .with_direction("New direction")
378            .with_session("sess-1");
379
380        let json = serde_json::to_string(&params).unwrap();
381        assert!(json.contains("cp-1"));
382        assert!(json.contains("New direction"));
383        assert!(json.contains("sess-1"));
384    }
385
386    #[test]
387    fn test_backtracking_params_deserialize() {
388        let json = r#"{
389            "checkpoint_id": "cp-123",
390            "new_direction": "Try option B",
391            "session_id": "sess-456",
392            "confidence": 0.9
393        }"#;
394
395        let params: BacktrackingParams = serde_json::from_str(json).unwrap();
396        assert_eq!(params.checkpoint_id, "cp-123");
397        assert_eq!(params.new_direction, Some("Try option B".to_string()));
398        assert_eq!(params.session_id, Some("sess-456".to_string()));
399        assert_eq!(params.confidence, 0.9);
400    }
401
402    #[test]
403    fn test_backtracking_params_deserialize_minimal() {
404        let json = r#"{"checkpoint_id": "cp-only"}"#;
405
406        let params: BacktrackingParams = serde_json::from_str(json).unwrap();
407        assert_eq!(params.checkpoint_id, "cp-only");
408        assert!(params.new_direction.is_none());
409        assert!(params.session_id.is_none());
410        assert_eq!(params.confidence, 0.8); // default
411    }
412
413    #[test]
414    fn test_backtracking_params_round_trip() {
415        let original = BacktrackingParams::new("cp-round")
416            .with_direction("Direction X")
417            .with_session("sess-round");
418
419        let json = serde_json::to_string(&original).unwrap();
420        let parsed: BacktrackingParams = serde_json::from_str(&json).unwrap();
421
422        assert_eq!(parsed.checkpoint_id, original.checkpoint_id);
423        assert_eq!(parsed.new_direction, original.new_direction);
424        assert_eq!(parsed.session_id, original.session_id);
425    }
426
427    // ============================================================================
428    // BacktrackingResponse Tests
429    // ============================================================================
430
431    #[test]
432    fn test_backtracking_response_from_json() {
433        let json = r#"{"thought": "New approach", "confidence": 0.9, "context_restored": true}"#;
434        let resp = BacktrackingResponse::from_completion(json).unwrap();
435        assert_eq!(resp.thought, "New approach");
436        assert_eq!(resp.confidence, 0.9);
437        assert!(resp.context_restored);
438    }
439
440    #[test]
441    fn test_backtracking_response_from_plain_text_returns_error() {
442        let text = "Just plain text";
443        // Non-JSON input returns error
444        let result = BacktrackingResponse::from_completion(text);
445        assert!(result.is_err());
446    }
447
448    #[test]
449    fn test_backtracking_response_with_all_fields() {
450        let json = r#"{
451            "thought": "Complete response",
452            "confidence": 0.95,
453            "context_restored": true,
454            "branch_from": "branch-abc",
455            "new_direction": "Exploring option C",
456            "metadata": {"key": "value"}
457        }"#;
458
459        let resp = BacktrackingResponse::from_completion(json).unwrap();
460        assert_eq!(resp.thought, "Complete response");
461        assert_eq!(resp.confidence, 0.95);
462        assert!(resp.context_restored);
463        assert_eq!(resp.branch_from, Some("branch-abc".to_string()));
464        assert_eq!(resp.new_direction, Some("Exploring option C".to_string()));
465        assert!(resp.metadata.is_some());
466    }
467
468    #[test]
469    fn test_backtracking_response_defaults() {
470        let json = r#"{"thought": "Minimal", "confidence": 0.7}"#;
471
472        let resp = BacktrackingResponse::from_completion(json).unwrap();
473        assert_eq!(resp.thought, "Minimal");
474        assert_eq!(resp.confidence, 0.7);
475        assert!(!resp.context_restored); // default is false
476        assert!(resp.branch_from.is_none());
477        assert!(resp.new_direction.is_none());
478        assert!(resp.metadata.is_none());
479    }
480
481    #[test]
482    fn test_backtracking_response_invalid_json_returns_error() {
483        let invalid = "{ invalid json }";
484
485        // All parse failures return errors (no fallbacks)
486        let result = BacktrackingResponse::from_completion(invalid);
487        assert!(result.is_err());
488        let err = result.unwrap_err();
489        assert!(matches!(err, ToolError::ParseFailed { mode, .. } if mode == "backtracking"));
490    }
491
492    #[test]
493    fn test_backtracking_response_empty_string_returns_error() {
494        let empty = "";
495
496        // Empty string is not valid JSON, returns error
497        let result = BacktrackingResponse::from_completion(empty);
498        assert!(result.is_err());
499    }
500
501    // ============================================================================
502    // BacktrackingResult Tests
503    // ============================================================================
504
505    #[test]
506    fn test_backtracking_result_serialize() {
507        let result = BacktrackingResult {
508            thought_id: "thought-123".to_string(),
509            session_id: "sess-456".to_string(),
510            checkpoint_restored: "cp-789".to_string(),
511            content: "Backtracked content".to_string(),
512            confidence: 0.85,
513            new_branch_id: Some("branch-abc".to_string()),
514            snapshot_id: "snap-xyz".to_string(),
515        };
516
517        let json = serde_json::to_string(&result).unwrap();
518        assert!(json.contains("thought-123"));
519        assert!(json.contains("cp-789"));
520        assert!(json.contains("Backtracked content"));
521        assert!(json.contains("0.85"));
522        assert!(json.contains("branch-abc"));
523    }
524
525    #[test]
526    fn test_backtracking_result_deserialize() {
527        let json = r#"{
528            "thought_id": "t-1",
529            "session_id": "s-1",
530            "checkpoint_restored": "cp-1",
531            "content": "Result content",
532            "confidence": 0.9,
533            "new_branch_id": "b-1",
534            "snapshot_id": "snap-1"
535        }"#;
536
537        let result: BacktrackingResult = serde_json::from_str(json).unwrap();
538        assert_eq!(result.thought_id, "t-1");
539        assert_eq!(result.session_id, "s-1");
540        assert_eq!(result.checkpoint_restored, "cp-1");
541        assert_eq!(result.content, "Result content");
542        assert_eq!(result.confidence, 0.9);
543        assert_eq!(result.new_branch_id, Some("b-1".to_string()));
544        assert_eq!(result.snapshot_id, "snap-1");
545    }
546
547    #[test]
548    fn test_backtracking_result_without_branch() {
549        let result = BacktrackingResult {
550            thought_id: "t-1".to_string(),
551            session_id: "s-1".to_string(),
552            checkpoint_restored: "cp-1".to_string(),
553            content: "No branch".to_string(),
554            confidence: 0.75,
555            new_branch_id: None,
556            snapshot_id: "snap-1".to_string(),
557        };
558
559        let json = serde_json::to_string(&result).unwrap();
560        let parsed: BacktrackingResult = serde_json::from_str(&json).unwrap();
561        assert!(parsed.new_branch_id.is_none());
562    }
563
564    #[test]
565    fn test_backtracking_result_round_trip() {
566        let original = BacktrackingResult {
567            thought_id: "round-t".to_string(),
568            session_id: "round-s".to_string(),
569            checkpoint_restored: "round-cp".to_string(),
570            content: "Round trip test".to_string(),
571            confidence: 0.88,
572            new_branch_id: Some("round-b".to_string()),
573            snapshot_id: "round-snap".to_string(),
574        };
575
576        let json = serde_json::to_string(&original).unwrap();
577        let parsed: BacktrackingResult = serde_json::from_str(&json).unwrap();
578
579        assert_eq!(parsed.thought_id, original.thought_id);
580        assert_eq!(parsed.session_id, original.session_id);
581        assert_eq!(parsed.checkpoint_restored, original.checkpoint_restored);
582        assert_eq!(parsed.content, original.content);
583        assert_eq!(parsed.confidence, original.confidence);
584        assert_eq!(parsed.new_branch_id, original.new_branch_id);
585        assert_eq!(parsed.snapshot_id, original.snapshot_id);
586    }
587
588    // ============================================================================
589    // Default Function Tests
590    // ============================================================================
591
592    #[test]
593    fn test_default_confidence() {
594        assert_eq!(default_confidence(), 0.8);
595    }
596
597    // ============================================================================
598    // Edge Case Tests
599    // ============================================================================
600
601    #[test]
602    fn test_backtracking_params_empty_checkpoint_id() {
603        let params = BacktrackingParams::new("");
604        assert_eq!(params.checkpoint_id, "");
605    }
606
607    #[test]
608    fn test_backtracking_params_unicode_direction() {
609        let params = BacktrackingParams::new("cp-1").with_direction("探索新方向 🔄");
610
611        assert_eq!(params.new_direction, Some("探索新方向 🔄".to_string()));
612    }
613
614    #[test]
615    fn test_backtracking_response_high_confidence() {
616        let json = r#"{"thought": "Very confident", "confidence": 1.0}"#;
617
618        let resp = BacktrackingResponse::from_completion(json).unwrap();
619        assert_eq!(resp.confidence, 1.0);
620    }
621
622    #[test]
623    fn test_backtracking_response_zero_confidence() {
624        let json = r#"{"thought": "No confidence", "confidence": 0.0}"#;
625
626        let resp = BacktrackingResponse::from_completion(json).unwrap();
627        assert_eq!(resp.confidence, 0.0);
628    }
629
630    #[test]
631    fn test_backtracking_response_with_complex_metadata() {
632        let json = r#"{
633            "thought": "With metadata",
634            "confidence": 0.8,
635            "metadata": {
636                "nested": {"key": "value"},
637                "array": [1, 2, 3],
638                "boolean": true
639            }
640        }"#;
641
642        let resp = BacktrackingResponse::from_completion(json).unwrap();
643        assert!(resp.metadata.is_some());
644        let meta = resp.metadata.unwrap();
645        assert!(meta.get("nested").is_some());
646        assert!(meta.get("array").is_some());
647    }
648
649    // ============================================================================
650    // Additional Edge Cases
651    // ============================================================================
652
653    #[test]
654    fn test_backtracking_params_confidence_edge_values() {
655        let json_min = r#"{"checkpoint_id": "cp-1", "confidence": 0.0}"#;
656        let json_max = r#"{"checkpoint_id": "cp-2", "confidence": 1.0}"#;
657
658        let params_min: BacktrackingParams = serde_json::from_str(json_min).unwrap();
659        let params_max: BacktrackingParams = serde_json::from_str(json_max).unwrap();
660
661        assert_eq!(params_min.confidence, 0.0);
662        assert_eq!(params_max.confidence, 1.0);
663    }
664
665    #[test]
666    fn test_backtracking_params_very_long_direction() {
667        let long_direction = "A".repeat(10000);
668        let params = BacktrackingParams::new("cp-1").with_direction(long_direction.clone());
669
670        assert_eq!(params.new_direction, Some(long_direction));
671    }
672
673    #[test]
674    fn test_backtracking_params_special_characters() {
675        let special = "Test with \n newlines \t tabs and \"quotes\" and 'apostrophes'";
676        let params = BacktrackingParams::new("cp-1").with_direction(special);
677
678        let json = serde_json::to_string(&params).unwrap();
679        let parsed: BacktrackingParams = serde_json::from_str(&json).unwrap();
680
681        assert_eq!(parsed.new_direction, Some(special.to_string()));
682    }
683
684    #[test]
685    fn test_backtracking_result_confidence_bounds() {
686        let result_low = BacktrackingResult {
687            thought_id: "t-low".to_string(),
688            session_id: "s-1".to_string(),
689            checkpoint_restored: "cp-1".to_string(),
690            content: "Low confidence".to_string(),
691            confidence: 0.0,
692            new_branch_id: None,
693            snapshot_id: "snap-1".to_string(),
694        };
695
696        let result_high = BacktrackingResult {
697            thought_id: "t-high".to_string(),
698            session_id: "s-1".to_string(),
699            checkpoint_restored: "cp-1".to_string(),
700            content: "High confidence".to_string(),
701            confidence: 1.0,
702            new_branch_id: None,
703            snapshot_id: "snap-1".to_string(),
704        };
705
706        assert_eq!(result_low.confidence, 0.0);
707        assert_eq!(result_high.confidence, 1.0);
708    }
709
710    #[test]
711    fn test_backtracking_response_malformed_but_valid_json() {
712        let json = r#"{"thought":"No spaces","confidence":0.5,"context_restored":false}"#;
713
714        let resp = BacktrackingResponse::from_completion(json).unwrap();
715        assert_eq!(resp.thought, "No spaces");
716        assert_eq!(resp.confidence, 0.5);
717        assert!(!resp.context_restored);
718    }
719
720    #[test]
721    fn test_backtracking_params_skip_serializing_none_fields() {
722        let params = BacktrackingParams::new("cp-1");
723
724        let json = serde_json::to_string(&params).unwrap();
725        // Fields with None should be skipped
726        assert!(!json.contains("new_direction"));
727        assert!(!json.contains("session_id"));
728    }
729
730    #[test]
731    fn test_backtracking_params_negative_confidence() {
732        let json = r#"{"checkpoint_id": "cp-1", "confidence": -0.5}"#;
733        let params: BacktrackingParams = serde_json::from_str(json).unwrap();
734        assert_eq!(params.confidence, -0.5);
735    }
736
737    #[test]
738    fn test_backtracking_params_very_high_confidence() {
739        let json = r#"{"checkpoint_id": "cp-1", "confidence": 99.9}"#;
740        let params: BacktrackingParams = serde_json::from_str(json).unwrap();
741        assert_eq!(params.confidence, 99.9);
742    }
743
744    #[test]
745    fn test_backtracking_response_with_null_metadata() {
746        let json = r#"{"thought": "Test", "confidence": 0.8, "metadata": null}"#;
747
748        let resp = BacktrackingResponse::from_completion(json).unwrap();
749        assert_eq!(resp.thought, "Test");
750        assert!(resp.metadata.is_none());
751    }
752
753    #[test]
754    fn test_backtracking_response_negative_confidence() {
755        let json = r#"{"thought": "Negative", "confidence": -1.0}"#;
756
757        let resp = BacktrackingResponse::from_completion(json).unwrap();
758        assert_eq!(resp.confidence, -1.0);
759    }
760
761    #[test]
762    fn test_backtracking_result_empty_strings() {
763        let result = BacktrackingResult {
764            thought_id: "".to_string(),
765            session_id: "".to_string(),
766            checkpoint_restored: "".to_string(),
767            content: "".to_string(),
768            confidence: 0.0,
769            new_branch_id: Some("".to_string()),
770            snapshot_id: "".to_string(),
771        };
772
773        let json = serde_json::to_string(&result).unwrap();
774        let parsed: BacktrackingResult = serde_json::from_str(&json).unwrap();
775
776        assert_eq!(parsed.thought_id, "");
777        assert_eq!(parsed.content, "");
778    }
779
780    #[test]
781    fn test_backtracking_params_builder_idempotency() {
782        let params1 = BacktrackingParams::new("cp-1")
783            .with_direction("dir-1")
784            .with_session("sess-1");
785
786        let params2 = BacktrackingParams::new("cp-1")
787            .with_direction("dir-1")
788            .with_session("sess-1");
789
790        assert_eq!(params1.checkpoint_id, params2.checkpoint_id);
791        assert_eq!(params1.new_direction, params2.new_direction);
792        assert_eq!(params1.session_id, params2.session_id);
793    }
794
795    #[test]
796    fn test_backtracking_response_extra_fields() {
797        let json = r#"{
798            "thought": "Test",
799            "confidence": 0.9,
800            "extra_unknown_field": "should be ignored",
801            "another_field": 123
802        }"#;
803
804        let resp = BacktrackingResponse::from_completion(json).unwrap();
805        assert_eq!(resp.thought, "Test");
806        assert_eq!(resp.confidence, 0.9);
807    }
808
809    #[test]
810    fn test_backtracking_params_overwrite_values() {
811        let params = BacktrackingParams::new("cp-original")
812            .with_direction("dir-1")
813            .with_direction("dir-2") // Overwrite
814            .with_session("sess-1")
815            .with_session("sess-2"); // Overwrite
816
817        assert_eq!(params.new_direction, Some("dir-2".to_string()));
818        assert_eq!(params.session_id, Some("sess-2".to_string()));
819    }
820
821    #[test]
822    fn test_backtracking_response_unicode_content() {
823        let json = r#"{"thought": "思考 🤔 émoji", "confidence": 0.8}"#;
824
825        let resp = BacktrackingResponse::from_completion(json).unwrap();
826        assert_eq!(resp.thought, "思考 🤔 émoji");
827    }
828
829    // ============================================================================
830    // BacktrackingMode Constructor Tests
831    // ============================================================================
832
833    #[test]
834    fn test_backtracking_mode_new() {
835        use crate::config::RequestConfig;
836        use crate::langbase::LangbaseClient;
837        use crate::storage::SqliteStorage;
838
839        let config = create_test_config();
840        let rt = tokio::runtime::Runtime::new().unwrap();
841        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
842        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
843
844        let mode = BacktrackingMode::new(storage, langbase, &config);
845        assert_eq!(mode.pipe_name, "backtracking-reasoning-v1");
846    }
847
848    #[test]
849    fn test_backtracking_mode_new_with_custom_pipe() {
850        use crate::config::{PipeConfig, RequestConfig};
851        use crate::langbase::LangbaseClient;
852        use crate::storage::SqliteStorage;
853
854        let mut config = create_test_config();
855        config.pipes = PipeConfig {
856            backtracking: Some("custom-backtracking-pipe".to_string()),
857            ..Default::default()
858        };
859
860        let rt = tokio::runtime::Runtime::new().unwrap();
861        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
862        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
863
864        let mode = BacktrackingMode::new(storage, langbase, &config);
865        assert_eq!(mode.pipe_name, "custom-backtracking-pipe");
866    }
867
868    // ============================================================================
869    // build_messages Tests
870    // ============================================================================
871
872    #[test]
873    fn test_build_messages_without_direction() {
874        use crate::config::RequestConfig;
875        use crate::langbase::LangbaseClient;
876        use crate::storage::{Checkpoint, SqliteStorage};
877        use serde_json::json;
878
879        let config = create_test_config();
880        let rt = tokio::runtime::Runtime::new().unwrap();
881        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
882        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
883        let mode = BacktrackingMode::new(storage, langbase, &config);
884
885        let checkpoint = Checkpoint::new(
886            "session-1",
887            "test-checkpoint",
888            json!({"thoughts": [], "branches": []}),
889        );
890
891        let messages = mode.build_messages(&checkpoint, None);
892
893        assert_eq!(messages.len(), 3);
894        assert!(matches!(
895            messages[0].role,
896            crate::langbase::MessageRole::System
897        ));
898        assert!(matches!(
899            messages[1].role,
900            crate::langbase::MessageRole::User
901        ));
902        assert!(messages[1].content.contains("test-checkpoint"));
903        assert!(matches!(
904            messages[2].role,
905            crate::langbase::MessageRole::User
906        ));
907        assert!(messages[2]
908            .content
909            .contains("Please continue reasoning from this checkpoint"));
910    }
911
912    #[test]
913    fn test_build_messages_with_direction() {
914        use crate::config::RequestConfig;
915        use crate::langbase::LangbaseClient;
916        use crate::storage::{Checkpoint, SqliteStorage};
917        use serde_json::json;
918
919        let config = create_test_config();
920        let rt = tokio::runtime::Runtime::new().unwrap();
921        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
922        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
923        let mode = BacktrackingMode::new(storage, langbase, &config);
924
925        let checkpoint = Checkpoint::new(
926            "session-1",
927            "test-checkpoint",
928            json!({"thoughts": [], "branches": []}),
929        );
930
931        let messages = mode.build_messages(&checkpoint, Some("Try a different approach"));
932
933        assert_eq!(messages.len(), 3);
934        assert!(matches!(
935            messages[0].role,
936            crate::langbase::MessageRole::System
937        ));
938        assert!(matches!(
939            messages[1].role,
940            crate::langbase::MessageRole::User
941        ));
942        assert!(messages[1].content.contains("test-checkpoint"));
943        assert!(matches!(
944            messages[2].role,
945            crate::langbase::MessageRole::User
946        ));
947        assert!(messages[2].content.contains("New direction to explore"));
948        assert!(messages[2].content.contains("Try a different approach"));
949    }
950
951    #[test]
952    fn test_build_messages_with_description() {
953        use crate::config::RequestConfig;
954        use crate::langbase::LangbaseClient;
955        use crate::storage::{Checkpoint, SqliteStorage};
956        use serde_json::json;
957
958        let config = create_test_config();
959        let rt = tokio::runtime::Runtime::new().unwrap();
960        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
961        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
962        let mode = BacktrackingMode::new(storage, langbase, &config);
963
964        let checkpoint = Checkpoint::new(
965            "session-1",
966            "test-checkpoint",
967            json!({"thoughts": [], "branches": []}),
968        )
969        .with_description("Important checkpoint");
970
971        let messages = mode.build_messages(&checkpoint, None);
972
973        assert_eq!(messages.len(), 3);
974        assert!(messages[1].content.contains("Important checkpoint"));
975        assert!(messages[1].content.contains("Description:"));
976    }
977
978    #[test]
979    fn test_build_messages_with_complex_snapshot() {
980        use crate::config::RequestConfig;
981        use crate::langbase::LangbaseClient;
982        use crate::storage::{Checkpoint, SqliteStorage};
983        use serde_json::json;
984
985        let config = create_test_config();
986        let rt = tokio::runtime::Runtime::new().unwrap();
987        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
988        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
989        let mode = BacktrackingMode::new(storage, langbase, &config);
990
991        let complex_snapshot = json!({
992            "thoughts": [
993                {"id": "t1", "content": "First thought"},
994                {"id": "t2", "content": "Second thought"}
995            ],
996            "branches": ["branch-a", "branch-b"],
997            "metadata": {"key": "value"}
998        });
999
1000        let checkpoint =
1001            Checkpoint::new("session-1", "complex-checkpoint", complex_snapshot.clone());
1002
1003        let messages = mode.build_messages(&checkpoint, None);
1004
1005        assert_eq!(messages.len(), 3);
1006        // The snapshot should be serialized as pretty JSON in the message
1007        assert!(messages[1].content.contains("complex-checkpoint"));
1008        assert!(messages[1].content.contains("thoughts"));
1009        assert!(messages[1].content.contains("branches"));
1010    }
1011
1012    #[test]
1013    fn test_build_messages_unicode_checkpoint_name() {
1014        use crate::config::RequestConfig;
1015        use crate::langbase::LangbaseClient;
1016        use crate::storage::{Checkpoint, SqliteStorage};
1017        use serde_json::json;
1018
1019        let config = create_test_config();
1020        let rt = tokio::runtime::Runtime::new().unwrap();
1021        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
1022        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
1023        let mode = BacktrackingMode::new(storage, langbase, &config);
1024
1025        let checkpoint = Checkpoint::new("session-1", "检查点 🔖", json!({"data": "test"}));
1026
1027        let messages = mode.build_messages(&checkpoint, Some("新方向 🚀"));
1028
1029        assert_eq!(messages.len(), 3);
1030        assert!(messages[1].content.contains("检查点 🔖"));
1031        assert!(messages[2].content.contains("新方向 🚀"));
1032    }
1033
1034    // ============================================================================
1035    // Clone Tests
1036    // ============================================================================
1037
1038    #[test]
1039    fn test_backtracking_mode_clone() {
1040        use crate::config::RequestConfig;
1041        use crate::langbase::LangbaseClient;
1042        use crate::storage::SqliteStorage;
1043
1044        let config = create_test_config();
1045        let rt = tokio::runtime::Runtime::new().unwrap();
1046        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
1047        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
1048
1049        let mode1 = BacktrackingMode::new(storage, langbase, &config);
1050        let mode2 = mode1.clone();
1051
1052        assert_eq!(mode1.pipe_name, mode2.pipe_name);
1053    }
1054
1055    // ============================================================================
1056    // Debug Trait Tests
1057    // ============================================================================
1058
1059    #[test]
1060    fn test_backtracking_params_debug() {
1061        let params = BacktrackingParams::new("cp-debug")
1062            .with_direction("debug direction")
1063            .with_session("sess-debug");
1064
1065        let debug_str = format!("{:?}", params);
1066        assert!(debug_str.contains("cp-debug"));
1067        assert!(debug_str.contains("debug direction"));
1068        assert!(debug_str.contains("sess-debug"));
1069    }
1070
1071    #[test]
1072    fn test_backtracking_result_debug() {
1073        let result = BacktrackingResult {
1074            thought_id: "t-debug".to_string(),
1075            session_id: "s-debug".to_string(),
1076            checkpoint_restored: "cp-debug".to_string(),
1077            content: "Debug content".to_string(),
1078            confidence: 0.9,
1079            new_branch_id: Some("b-debug".to_string()),
1080            snapshot_id: "snap-debug".to_string(),
1081        };
1082
1083        let debug_str = format!("{:?}", result);
1084        assert!(debug_str.contains("t-debug"));
1085        assert!(debug_str.contains("s-debug"));
1086        assert!(debug_str.contains("cp-debug"));
1087    }
1088
1089    // ============================================================================
1090    // Additional Serialization Edge Cases
1091    // ============================================================================
1092
1093    #[test]
1094    fn test_backtracking_params_with_float_precision() {
1095        let json = r#"{"checkpoint_id": "cp-1", "confidence": 0.123456789}"#;
1096        let params: BacktrackingParams = serde_json::from_str(json).unwrap();
1097        assert_eq!(params.confidence, 0.123456789);
1098    }
1099
1100    #[test]
1101    fn test_backtracking_response_with_escaped_quotes() {
1102        let json = r#"{"thought": "She said \"hello\"", "confidence": 0.8}"#;
1103
1104        let resp = BacktrackingResponse::from_completion(json).unwrap();
1105        assert_eq!(resp.thought, "She said \"hello\"");
1106    }
1107
1108    #[test]
1109    fn test_backtracking_response_with_newlines() {
1110        let json = r#"{"thought": "Line 1\nLine 2\nLine 3", "confidence": 0.8}"#;
1111
1112        let resp = BacktrackingResponse::from_completion(json).unwrap();
1113        assert_eq!(resp.thought, "Line 1\nLine 2\nLine 3");
1114    }
1115
1116    #[test]
1117    fn test_backtracking_result_with_unicode_ids() {
1118        let result = BacktrackingResult {
1119            thought_id: "思考-123".to_string(),
1120            session_id: "会话-456".to_string(),
1121            checkpoint_restored: "检查点-789".to_string(),
1122            content: "Unicode content".to_string(),
1123            confidence: 0.85,
1124            new_branch_id: None,
1125            snapshot_id: "快照-xyz".to_string(),
1126        };
1127
1128        let json = serde_json::to_string(&result).unwrap();
1129        let parsed: BacktrackingResult = serde_json::from_str(&json).unwrap();
1130
1131        assert_eq!(parsed.thought_id, "思考-123");
1132        assert_eq!(parsed.session_id, "会话-456");
1133        assert_eq!(parsed.checkpoint_restored, "检查点-789");
1134        assert_eq!(parsed.snapshot_id, "快照-xyz");
1135    }
1136
1137    #[test]
1138    fn test_backtracking_params_from_into_string() {
1139        // Test that Into<String> trait works with &str
1140        let params1 = BacktrackingParams::new("checkpoint-from-str");
1141        let params2 = BacktrackingParams::new(String::from("checkpoint-from-string"));
1142
1143        assert_eq!(params1.checkpoint_id, "checkpoint-from-str");
1144        assert_eq!(params2.checkpoint_id, "checkpoint-from-string");
1145    }
1146
1147    #[test]
1148    fn test_backtracking_response_missing_required_fields_returns_error() {
1149        // Test when required fields are missing - returns error
1150        let json = r#"{"confidence": 0.9}"#; // Missing "thought"
1151
1152        let result = BacktrackingResponse::from_completion(json);
1153        assert!(result.is_err());
1154    }
1155
1156    #[test]
1157    fn test_backtracking_response_very_long_thought() {
1158        let long_thought = "A".repeat(100000);
1159        let json = format!(r#"{{"thought": "{}", "confidence": 0.8}}"#, long_thought);
1160
1161        let resp = BacktrackingResponse::from_completion(&json).unwrap();
1162        assert_eq!(resp.thought, long_thought);
1163    }
1164
1165    #[test]
1166    fn test_backtracking_params_clone() {
1167        let params1 = BacktrackingParams::new("cp-clone")
1168            .with_direction("Clone test")
1169            .with_session("sess-clone");
1170
1171        let params2 = params1.clone();
1172
1173        assert_eq!(params1.checkpoint_id, params2.checkpoint_id);
1174        assert_eq!(params1.new_direction, params2.new_direction);
1175        assert_eq!(params1.session_id, params2.session_id);
1176        assert_eq!(params1.confidence, params2.confidence);
1177    }
1178
1179    #[test]
1180    fn test_backtracking_result_clone() {
1181        let result1 = BacktrackingResult {
1182            thought_id: "t-clone".to_string(),
1183            session_id: "s-clone".to_string(),
1184            checkpoint_restored: "cp-clone".to_string(),
1185            content: "Clone test".to_string(),
1186            confidence: 0.9,
1187            new_branch_id: Some("b-clone".to_string()),
1188            snapshot_id: "snap-clone".to_string(),
1189        };
1190
1191        let result2 = result1.clone();
1192
1193        assert_eq!(result1.thought_id, result2.thought_id);
1194        assert_eq!(result1.session_id, result2.session_id);
1195        assert_eq!(result1.checkpoint_restored, result2.checkpoint_restored);
1196        assert_eq!(result1.content, result2.content);
1197        assert_eq!(result1.confidence, result2.confidence);
1198        assert_eq!(result1.new_branch_id, result2.new_branch_id);
1199        assert_eq!(result1.snapshot_id, result2.snapshot_id);
1200    }
1201
1202    #[test]
1203    fn test_backtracking_response_clone() {
1204        let json = r#"{"thought": "Clone test", "confidence": 0.9, "context_restored": true}"#;
1205        let resp1 = BacktrackingResponse::from_completion(json).unwrap();
1206        let resp2 = resp1.clone();
1207
1208        assert_eq!(resp1.thought, resp2.thought);
1209        assert_eq!(resp1.confidence, resp2.confidence);
1210        assert_eq!(resp1.context_restored, resp2.context_restored);
1211    }
1212
1213    // ============================================================================
1214    // Confidence Value Tests
1215    // ============================================================================
1216
1217    #[test]
1218    fn test_backtracking_params_fractional_confidence() {
1219        let json = r#"{"checkpoint_id": "cp-1", "confidence": 0.123}"#;
1220        let params: BacktrackingParams = serde_json::from_str(json).unwrap();
1221        assert_eq!(params.confidence, 0.123);
1222    }
1223
1224    #[test]
1225    fn test_backtracking_response_scientific_notation() {
1226        let json = r#"{"thought": "Test", "confidence": 1e-10}"#;
1227
1228        let resp = BacktrackingResponse::from_completion(json).unwrap();
1229        assert_eq!(resp.confidence, 1e-10);
1230    }
1231
1232    #[test]
1233    fn test_backtracking_result_with_very_small_confidence() {
1234        let result = BacktrackingResult {
1235            thought_id: "t-1".to_string(),
1236            session_id: "s-1".to_string(),
1237            checkpoint_restored: "cp-1".to_string(),
1238            content: "Very low confidence".to_string(),
1239            confidence: 0.0001,
1240            new_branch_id: None,
1241            snapshot_id: "snap-1".to_string(),
1242        };
1243
1244        let json = serde_json::to_string(&result).unwrap();
1245        let parsed: BacktrackingResult = serde_json::from_str(&json).unwrap();
1246
1247        assert_eq!(parsed.confidence, 0.0001);
1248    }
1249}