1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct BacktrackingParams {
17 pub checkpoint_id: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub new_direction: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub session_id: Option<String>,
25 #[serde(default = "default_confidence")]
27 pub confidence: f64,
28}
29
30fn default_confidence() -> f64 {
31 0.8
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct BacktrackingResult {
37 pub thought_id: String,
39 pub session_id: String,
41 pub checkpoint_restored: String,
43 pub content: String,
45 pub confidence: f64,
47 pub new_branch_id: Option<String>,
49 pub snapshot_id: String,
51}
52
53#[allow(dead_code)] #[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 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#[derive(Clone)]
84pub struct BacktrackingMode {
85 core: ModeCore,
87 pipe_name: String,
89}
90
91impl BacktrackingMode {
92 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 pub async fn process(&self, params: BacktrackingParams) -> AppResult<BacktrackingResult> {
106 let start = Instant::now();
107
108 let checkpoint = self
110 .core
111 .storage()
112 .get_checkpoint(¶ms.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 let session =
123 match ¶ms.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 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 let messages = self.build_messages(&checkpoint, params.new_direction.as_deref());
162
163 let request = PipeRequest::new(&self.pipe_name, messages);
165 let response = self.core.langbase().call_pipe(request).await?;
166
167 let backtrack_response = BacktrackingResponse::from_completion(&response.completion)?;
169
170 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 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 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 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 pub async fn create_checkpoint(
234 &self,
235 session_id: &str,
236 name: &str,
237 description: Option<&str>,
238 ) -> AppResult<Checkpoint> {
239 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 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 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 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 pub fn with_direction(mut self, direction: impl Into<String>) -> Self {
289 self.new_direction = Some(direction.into());
290 self
291 }
292
293 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 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 #[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(¶ms).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); }
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 #[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 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); 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 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 let result = BacktrackingResponse::from_completion(empty);
498 assert!(result.is_err());
499 }
500
501 #[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 #[test]
593 fn test_default_confidence() {
594 assert_eq!(default_confidence(), 0.8);
595 }
596
597 #[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 #[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(¶ms).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(¶ms).unwrap();
725 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") .with_session("sess-1")
815 .with_session("sess-2"); 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 #[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 #[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 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 #[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 #[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 #[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 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 let json = r#"{"confidence": 0.9}"#; 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 #[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}