mcp_langbase_reasoning/modes/
linear.rs

1//! Linear reasoning mode - sequential step-by-step reasoning.
2//!
3//! This module provides linear reasoning for step-by-step thought processing:
4//! - Single-pass sequential reasoning
5//! - Session continuity with thought history
6//! - Confidence tracking
7
8use serde::{Deserialize, Serialize};
9use std::time::Instant;
10use tracing::{debug, info};
11
12use super::{serialize_for_log, ModeCore};
13use crate::config::Config;
14use crate::error::{AppResult, ToolError};
15use crate::langbase::{LangbaseClient, Message, PipeRequest, ReasoningResponse};
16use crate::prompts::LINEAR_REASONING_PROMPT;
17use crate::storage::{Invocation, SqliteStorage, Storage, Thought};
18
19/// Input parameters for linear reasoning
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct LinearParams {
22    /// The thought content to process
23    pub content: String,
24    /// Optional session ID (creates new if not provided)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub session_id: Option<String>,
27    /// Confidence threshold (0.0-1.0)
28    #[serde(default = "default_confidence")]
29    pub confidence: f64,
30}
31
32fn default_confidence() -> f64 {
33    0.8
34}
35
36/// Result of linear reasoning.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LinearResult {
39    /// The ID of the created thought.
40    pub thought_id: String,
41    /// The session ID.
42    pub session_id: String,
43    /// The processed thought content.
44    pub content: String,
45    /// Confidence in the reasoning (0.0-1.0).
46    pub confidence: f64,
47    /// The ID of the previous thought in the chain, if any.
48    pub previous_thought: Option<String>,
49}
50
51/// Linear reasoning mode handler for sequential reasoning.
52#[derive(Clone)]
53pub struct LinearMode {
54    /// Core infrastructure (storage and langbase client).
55    core: ModeCore,
56    /// The Langbase pipe name for linear reasoning.
57    pipe_name: String,
58}
59
60impl LinearMode {
61    /// Create a new linear mode handler
62    pub fn new(storage: SqliteStorage, langbase: LangbaseClient, config: &Config) -> Self {
63        Self {
64            core: ModeCore::new(storage, langbase),
65            pipe_name: config.pipes.linear.clone(),
66        }
67    }
68
69    /// Process a linear reasoning request
70    pub async fn process(&self, params: LinearParams) -> AppResult<LinearResult> {
71        let start = Instant::now();
72
73        // Validate input
74        if params.content.trim().is_empty() {
75            return Err(ToolError::Validation {
76                field: "content".to_string(),
77                reason: "Content cannot be empty".to_string(),
78            }
79            .into());
80        }
81
82        // Get or create session
83        let session = self
84            .core
85            .storage()
86            .get_or_create_session(&params.session_id, "linear")
87            .await?;
88
89        debug!(session_id = %session.id, "Processing linear reasoning");
90
91        // Get previous thoughts for context
92        let previous_thoughts = self
93            .core
94            .storage()
95            .get_session_thoughts(&session.id)
96            .await?;
97        let previous_thought = previous_thoughts.last().cloned();
98
99        // Build context for Langbase
100        let messages = self.build_messages(&params.content, &previous_thoughts);
101
102        // Create invocation log
103        let mut invocation = Invocation::new(
104            "reasoning.linear",
105            serialize_for_log(&params, "reasoning.linear input"),
106        )
107        .with_session(&session.id)
108        .with_pipe(&self.pipe_name);
109
110        // Call Langbase pipe
111        let request = PipeRequest::new(&self.pipe_name, messages);
112        let response = match self.core.langbase().call_pipe(request).await {
113            Ok(resp) => resp,
114            Err(e) => {
115                let latency = start.elapsed().as_millis() as i64;
116                invocation = invocation.failure(e.to_string(), latency);
117                self.core.storage().log_invocation(&invocation).await?;
118                return Err(e.into());
119            }
120        };
121
122        // Parse response
123        let reasoning = ReasoningResponse::from_completion(&response.completion);
124
125        // Create and store thought
126        let thought = Thought::new(&session.id, &reasoning.thought, "linear")
127            .with_confidence(reasoning.confidence.max(params.confidence));
128
129        self.core.storage().create_thought(&thought).await?;
130
131        // Log successful invocation
132        let latency = start.elapsed().as_millis() as i64;
133        invocation = invocation.success(
134            serialize_for_log(&reasoning, "reasoning.linear output"),
135            latency,
136        );
137        self.core.storage().log_invocation(&invocation).await?;
138
139        info!(
140            session_id = %session.id,
141            thought_id = %thought.id,
142            latency_ms = latency,
143            "Linear reasoning completed"
144        );
145
146        Ok(LinearResult {
147            thought_id: thought.id,
148            session_id: session.id,
149            content: reasoning.thought,
150            confidence: reasoning.confidence,
151            previous_thought: previous_thought.map(|t| t.id),
152        })
153    }
154
155    /// Build messages for the Langbase pipe
156    fn build_messages(&self, content: &str, history: &[Thought]) -> Vec<Message> {
157        let mut messages = Vec::new();
158
159        // System prompt for linear reasoning (from centralized prompts module)
160        messages.push(Message::system(LINEAR_REASONING_PROMPT));
161
162        // Add history context if available
163        if !history.is_empty() {
164            let history_text: Vec<String> =
165                history.iter().map(|t| format!("- {}", t.content)).collect();
166
167            messages.push(Message::user(format!(
168                "Previous reasoning steps:\n{}\n\nNow process this thought:",
169                history_text.join("\n")
170            )));
171        }
172
173        // Add current content
174        messages.push(Message::user(content.to_string()));
175
176        messages
177    }
178}
179
180impl LinearParams {
181    /// Create new params with just content
182    pub fn new(content: impl Into<String>) -> Self {
183        Self {
184            content: content.into(),
185            session_id: None,
186            confidence: default_confidence(),
187        }
188    }
189
190    /// Set the session ID
191    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
192        self.session_id = Some(session_id.into());
193        self
194    }
195
196    /// Set the confidence threshold
197    pub fn with_confidence(mut self, confidence: f64) -> Self {
198        self.confidence = confidence.clamp(0.0, 1.0);
199        self
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::config::RequestConfig;
207    use crate::langbase::MessageRole;
208
209    // ============================================================================
210    // LinearParams Tests
211    // ============================================================================
212
213    #[test]
214    fn test_linear_params_new() {
215        let params = LinearParams::new("Test content");
216        assert_eq!(params.content, "Test content");
217        assert!(params.session_id.is_none());
218        assert_eq!(params.confidence, 0.8);
219    }
220
221    #[test]
222    fn test_linear_params_with_session() {
223        let params = LinearParams::new("Content").with_session("sess-123");
224        assert_eq!(params.session_id, Some("sess-123".to_string()));
225    }
226
227    #[test]
228    fn test_linear_params_with_confidence() {
229        let params = LinearParams::new("Content").with_confidence(0.9);
230        assert_eq!(params.confidence, 0.9);
231    }
232
233    #[test]
234    fn test_linear_params_confidence_clamped_high() {
235        let params = LinearParams::new("Content").with_confidence(1.5);
236        assert_eq!(params.confidence, 1.0);
237    }
238
239    #[test]
240    fn test_linear_params_confidence_clamped_low() {
241        let params = LinearParams::new("Content").with_confidence(-0.5);
242        assert_eq!(params.confidence, 0.0);
243    }
244
245    #[test]
246    fn test_linear_params_builder_chain() {
247        let params = LinearParams::new("Chained")
248            .with_session("my-session")
249            .with_confidence(0.75);
250
251        assert_eq!(params.content, "Chained");
252        assert_eq!(params.session_id, Some("my-session".to_string()));
253        assert_eq!(params.confidence, 0.75);
254    }
255
256    #[test]
257    fn test_linear_params_serialize() {
258        let params = LinearParams::new("Test")
259            .with_session("sess-1")
260            .with_confidence(0.85);
261
262        let json = serde_json::to_string(&params).unwrap();
263        assert!(json.contains("Test"));
264        assert!(json.contains("sess-1"));
265        assert!(json.contains("0.85"));
266    }
267
268    #[test]
269    fn test_linear_params_deserialize() {
270        let json = r#"{"content": "Parsed", "session_id": "s-1", "confidence": 0.9}"#;
271        let params: LinearParams = serde_json::from_str(json).unwrap();
272
273        assert_eq!(params.content, "Parsed");
274        assert_eq!(params.session_id, Some("s-1".to_string()));
275        assert_eq!(params.confidence, 0.9);
276    }
277
278    #[test]
279    fn test_linear_params_deserialize_minimal() {
280        let json = r#"{"content": "Only content"}"#;
281        let params: LinearParams = serde_json::from_str(json).unwrap();
282
283        assert_eq!(params.content, "Only content");
284        assert!(params.session_id.is_none());
285        assert_eq!(params.confidence, 0.8); // default
286    }
287
288    // ============================================================================
289    // LinearResult Tests
290    // ============================================================================
291
292    #[test]
293    fn test_linear_result_serialize() {
294        let result = LinearResult {
295            thought_id: "thought-123".to_string(),
296            session_id: "sess-456".to_string(),
297            content: "Reasoning output".to_string(),
298            confidence: 0.88,
299            previous_thought: Some("thought-122".to_string()),
300        };
301
302        let json = serde_json::to_string(&result).unwrap();
303        assert!(json.contains("thought-123"));
304        assert!(json.contains("sess-456"));
305        assert!(json.contains("Reasoning output"));
306        assert!(json.contains("0.88"));
307    }
308
309    #[test]
310    fn test_linear_result_deserialize() {
311        let json = r#"{
312            "thought_id": "t-1",
313            "session_id": "s-1",
314            "content": "Result content",
315            "confidence": 0.95,
316            "previous_thought": "t-0"
317        }"#;
318
319        let result: LinearResult = serde_json::from_str(json).unwrap();
320        assert_eq!(result.thought_id, "t-1");
321        assert_eq!(result.session_id, "s-1");
322        assert_eq!(result.content, "Result content");
323        assert_eq!(result.confidence, 0.95);
324        assert_eq!(result.previous_thought, Some("t-0".to_string()));
325    }
326
327    #[test]
328    fn test_linear_result_without_previous() {
329        let result = LinearResult {
330            thought_id: "t-1".to_string(),
331            session_id: "s-1".to_string(),
332            content: "First thought".to_string(),
333            confidence: 0.8,
334            previous_thought: None,
335        };
336
337        let json = serde_json::to_string(&result).unwrap();
338        let parsed: LinearResult = serde_json::from_str(&json).unwrap();
339        assert!(parsed.previous_thought.is_none());
340    }
341
342    // ============================================================================
343    // Default Function Tests
344    // ============================================================================
345
346    #[test]
347    fn test_default_confidence() {
348        assert_eq!(default_confidence(), 0.8);
349    }
350
351    // ============================================================================
352    // Edge Cases - Content Handling
353    // ============================================================================
354
355    #[test]
356    fn test_linear_params_empty_content() {
357        let params = LinearParams::new("");
358        assert_eq!(params.content, "");
359    }
360
361    #[test]
362    fn test_linear_params_very_long_content() {
363        let long_content = "a".repeat(10000);
364        let params = LinearParams::new(long_content.clone());
365        assert_eq!(params.content, long_content);
366        assert_eq!(params.content.len(), 10000);
367    }
368
369    #[test]
370    fn test_linear_params_special_characters() {
371        let special = "Test with special: \n\t\r\"'\\{}[]()!@#$%^&*";
372        let params = LinearParams::new(special);
373        assert_eq!(params.content, special);
374    }
375
376    #[test]
377    fn test_linear_params_unicode_content() {
378        let unicode = "Hello ไธ–็•Œ ๐ŸŒ ะŸั€ะธะฒะตั‚ ู…ุฑุญุจุง";
379        let params = LinearParams::new(unicode);
380        assert_eq!(params.content, unicode);
381    }
382
383    #[test]
384    fn test_linear_params_multiline_content() {
385        let multiline = "Line 1\nLine 2\nLine 3\nLine 4";
386        let params = LinearParams::new(multiline);
387        assert_eq!(params.content, multiline);
388        assert!(params.content.contains('\n'));
389    }
390
391    #[test]
392    fn test_linear_params_whitespace_only() {
393        let whitespace = "   \t\n  ";
394        let params = LinearParams::new(whitespace);
395        assert_eq!(params.content, whitespace);
396    }
397
398    // ============================================================================
399    // Confidence Edge Cases
400    // ============================================================================
401
402    #[test]
403    fn test_linear_params_confidence_exactly_zero() {
404        let params = LinearParams::new("Test").with_confidence(0.0);
405        assert_eq!(params.confidence, 0.0);
406    }
407
408    #[test]
409    fn test_linear_params_confidence_exactly_one() {
410        let params = LinearParams::new("Test").with_confidence(1.0);
411        assert_eq!(params.confidence, 1.0);
412    }
413
414    #[test]
415    fn test_linear_params_confidence_exactly_half() {
416        let params = LinearParams::new("Test").with_confidence(0.5);
417        assert_eq!(params.confidence, 0.5);
418    }
419
420    #[test]
421    fn test_linear_params_confidence_very_negative() {
422        let params = LinearParams::new("Test").with_confidence(-999.9);
423        assert_eq!(params.confidence, 0.0);
424    }
425
426    #[test]
427    fn test_linear_params_confidence_very_positive() {
428        let params = LinearParams::new("Test").with_confidence(999.9);
429        assert_eq!(params.confidence, 1.0);
430    }
431
432    // ============================================================================
433    // ReasoningResponse::from_completion Tests
434    // ============================================================================
435
436    #[test]
437    fn test_reasoning_response_valid_json() {
438        let json = r#"{"thought": "Test thought", "confidence": 0.95, "metadata": null}"#;
439        let response = ReasoningResponse::from_completion(json);
440        assert_eq!(response.thought, "Test thought");
441        assert_eq!(response.confidence, 0.95);
442        assert!(response.metadata.is_none());
443    }
444
445    #[test]
446    fn test_reasoning_response_with_metadata() {
447        let json =
448            r#"{"thought": "Meta thought", "confidence": 0.88, "metadata": {"key": "value"}}"#;
449        let response = ReasoningResponse::from_completion(json);
450        assert_eq!(response.thought, "Meta thought");
451        assert_eq!(response.confidence, 0.88);
452        assert!(response.metadata.is_some());
453    }
454
455    #[test]
456    fn test_reasoning_response_invalid_json_fallback() {
457        let invalid = "This is not JSON at all";
458        let response = ReasoningResponse::from_completion(invalid);
459        assert_eq!(response.thought, invalid);
460        assert_eq!(response.confidence, 0.8);
461        assert!(response.metadata.is_none());
462    }
463
464    #[test]
465    fn test_reasoning_response_partial_json_fallback() {
466        let partial = r#"{"thought": "incomplete""#;
467        let response = ReasoningResponse::from_completion(partial);
468        assert_eq!(response.thought, partial);
469        assert_eq!(response.confidence, 0.8);
470    }
471
472    #[test]
473    fn test_reasoning_response_empty_string_fallback() {
474        let empty = "";
475        let response = ReasoningResponse::from_completion(empty);
476        assert_eq!(response.thought, empty);
477        assert_eq!(response.confidence, 0.8);
478    }
479
480    #[test]
481    fn test_reasoning_response_json_with_special_chars() {
482        let json = r#"{"thought": "Special: \n\t\"quote\"", "confidence": 0.9, "metadata": null}"#;
483        let response = ReasoningResponse::from_completion(json);
484        assert!(response.thought.contains("Special"));
485        assert_eq!(response.confidence, 0.9);
486    }
487
488    #[test]
489    fn test_reasoning_response_minimal_valid_json() {
490        let json = r#"{"thought": "T", "confidence": 0.1}"#;
491        let response = ReasoningResponse::from_completion(json);
492        assert_eq!(response.thought, "T");
493        assert_eq!(response.confidence, 0.1);
494    }
495
496    #[test]
497    fn test_reasoning_response_unicode_in_json() {
498        let json = r#"{"thought": "Unicode: ไธ–็•Œ ๐ŸŒ", "confidence": 0.85, "metadata": null}"#;
499        let response = ReasoningResponse::from_completion(json);
500        assert!(response.thought.contains("ไธ–็•Œ"));
501        assert!(response.thought.contains("๐ŸŒ"));
502        assert_eq!(response.confidence, 0.85);
503    }
504
505    // ============================================================================
506    // Note: build_messages Tests
507    // ============================================================================
508    // build_messages is adequately tested through integration tests.
509    // Direct unit testing requires complex setup (async runtime, proper Config
510    // construction) and is better suited for integration test files.
511
512    // ============================================================================
513    // Serialization Edge Cases
514    // ============================================================================
515
516    #[test]
517    fn test_linear_params_skip_none_session() {
518        let params = LinearParams::new("Test");
519        let json = serde_json::to_string(&params).unwrap();
520        // session_id should not appear in JSON when None
521        assert!(!json.contains("session_id"));
522    }
523
524    #[test]
525    fn test_linear_params_roundtrip() {
526        let original = LinearParams::new("Roundtrip test")
527            .with_session("sess-rt")
528            .with_confidence(0.77);
529
530        let json = serde_json::to_string(&original).unwrap();
531        let parsed: LinearParams = serde_json::from_str(&json).unwrap();
532
533        assert_eq!(parsed.content, original.content);
534        assert_eq!(parsed.session_id, original.session_id);
535        assert_eq!(parsed.confidence, original.confidence);
536    }
537
538    #[test]
539    fn test_linear_result_roundtrip() {
540        let original = LinearResult {
541            thought_id: "t-123".to_string(),
542            session_id: "s-456".to_string(),
543            content: "Test content".to_string(),
544            confidence: 0.92,
545            previous_thought: Some("t-122".to_string()),
546        };
547
548        let json = serde_json::to_string(&original).unwrap();
549        let parsed: LinearResult = serde_json::from_str(&json).unwrap();
550
551        assert_eq!(parsed.thought_id, original.thought_id);
552        assert_eq!(parsed.session_id, original.session_id);
553        assert_eq!(parsed.content, original.content);
554        assert_eq!(parsed.confidence, original.confidence);
555        assert_eq!(parsed.previous_thought, original.previous_thought);
556    }
557
558    #[test]
559    fn test_linear_params_deserialize_with_extra_fields() {
560        // Should ignore unknown fields
561        let json = r#"{
562            "content": "Test",
563            "session_id": "s-1",
564            "confidence": 0.9,
565            "unknown_field": "should be ignored"
566        }"#;
567
568        let params: LinearParams = serde_json::from_str(json).unwrap();
569        assert_eq!(params.content, "Test");
570        assert_eq!(params.session_id, Some("s-1".to_string()));
571        assert_eq!(params.confidence, 0.9);
572    }
573
574    #[test]
575    fn test_linear_result_serialize_with_none_previous() {
576        let result = LinearResult {
577            thought_id: "t-1".to_string(),
578            session_id: "s-1".to_string(),
579            content: "Content".to_string(),
580            confidence: 0.8,
581            previous_thought: None,
582        };
583
584        let json = serde_json::to_string(&result).unwrap();
585        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
586
587        // previous_thought should be null in JSON
588        assert_eq!(parsed["previous_thought"], serde_json::Value::Null);
589    }
590
591    // ============================================================================
592    // Builder Pattern Edge Cases
593    // ============================================================================
594
595    #[test]
596    fn test_linear_params_multiple_session_overwrites() {
597        let params = LinearParams::new("Test")
598            .with_session("first")
599            .with_session("second")
600            .with_session("third");
601
602        assert_eq!(params.session_id, Some("third".to_string()));
603    }
604
605    #[test]
606    fn test_linear_params_multiple_confidence_overwrites() {
607        let params = LinearParams::new("Test")
608            .with_confidence(0.5)
609            .with_confidence(0.7)
610            .with_confidence(0.9);
611
612        assert_eq!(params.confidence, 0.9);
613    }
614
615    #[test]
616    fn test_linear_params_string_types() {
617        // Test that Into<String> works for various string types
618        let owned = String::from("owned");
619        let params1 = LinearParams::new(owned);
620        assert_eq!(params1.content, "owned");
621
622        let borrowed = "borrowed";
623        let params2 = LinearParams::new(borrowed);
624        assert_eq!(params2.content, "borrowed");
625
626        let params3 = LinearParams::new("literal".to_string());
627        assert_eq!(params3.content, "literal");
628    }
629
630    #[test]
631    fn test_linear_params_session_string_types() {
632        let params = LinearParams::new("Test")
633            .with_session("literal")
634            .with_session(String::from("owned"));
635
636        assert_eq!(params.session_id, Some("owned".to_string()));
637    }
638
639    // ============================================================================
640    // LinearMode Tests
641    // ============================================================================
642
643    fn create_test_config() -> Config {
644        use crate::config::{
645            DatabaseConfig, ErrorHandlingConfig, LangbaseConfig, LogFormat, LoggingConfig,
646            PipeConfig, RequestConfig,
647        };
648        use std::path::PathBuf;
649
650        Config {
651            langbase: LangbaseConfig {
652                api_key: "test-key".to_string(),
653                base_url: "https://api.langbase.com".to_string(),
654            },
655            database: DatabaseConfig {
656                path: PathBuf::from(":memory:"),
657                max_connections: 5,
658            },
659            logging: LoggingConfig {
660                level: "info".to_string(),
661                format: LogFormat::Pretty,
662            },
663            request: RequestConfig::default(),
664            pipes: PipeConfig::default(),
665            error_handling: ErrorHandlingConfig::default(),
666        }
667    }
668
669    #[test]
670    fn test_linear_mode_new() {
671        let config = create_test_config();
672        let rt = tokio::runtime::Runtime::new().unwrap();
673        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
674        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
675
676        let mode = LinearMode::new(storage, langbase, &config);
677        assert_eq!(mode.pipe_name, "linear-reasoning-v1");
678    }
679
680    #[test]
681    fn test_linear_mode_new_with_custom_pipe() {
682        let mut config = create_test_config();
683        config.pipes.linear = "custom-linear-pipe".to_string();
684
685        let rt = tokio::runtime::Runtime::new().unwrap();
686        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
687        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
688
689        let mode = LinearMode::new(storage, langbase, &config);
690        assert_eq!(mode.pipe_name, "custom-linear-pipe");
691    }
692
693    #[test]
694    fn test_linear_mode_clone() {
695        let config = create_test_config();
696        let rt = tokio::runtime::Runtime::new().unwrap();
697        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
698        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
699
700        let mode = LinearMode::new(storage, langbase, &config);
701        let cloned = mode.clone();
702        assert_eq!(mode.pipe_name, cloned.pipe_name);
703    }
704
705    #[test]
706    fn test_build_messages_empty_history() {
707        let config = create_test_config();
708        let rt = tokio::runtime::Runtime::new().unwrap();
709        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
710        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
711
712        let mode = LinearMode::new(storage, langbase, &config);
713        let messages = mode.build_messages("Test content", &[]);
714
715        // Should have 2 messages: system prompt + user content
716        assert_eq!(messages.len(), 2);
717        assert!(matches!(messages[0].role, MessageRole::System));
718        assert!(matches!(messages[1].role, MessageRole::User));
719        assert_eq!(messages[1].content, "Test content");
720    }
721
722    #[test]
723    fn test_build_messages_with_history() {
724        let config = create_test_config();
725        let rt = tokio::runtime::Runtime::new().unwrap();
726        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
727        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
728
729        let mode = LinearMode::new(storage, langbase, &config);
730
731        // Create mock history thoughts
732        let history = vec![
733            Thought::new("sess-1", "First thought", "linear"),
734            Thought::new("sess-1", "Second thought", "linear"),
735        ];
736
737        let messages = mode.build_messages("New content", &history);
738
739        // Should have 3 messages: system prompt + history context + user content
740        assert_eq!(messages.len(), 3);
741        assert!(matches!(messages[0].role, MessageRole::System));
742        assert!(matches!(messages[1].role, MessageRole::User));
743        assert!(messages[1].content.contains("Previous reasoning steps:"));
744        assert!(messages[1].content.contains("First thought"));
745        assert!(messages[1].content.contains("Second thought"));
746        assert!(matches!(messages[2].role, MessageRole::User));
747        assert_eq!(messages[2].content, "New content");
748    }
749
750    #[test]
751    fn test_build_messages_with_single_history() {
752        let config = create_test_config();
753        let rt = tokio::runtime::Runtime::new().unwrap();
754        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
755        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
756
757        let mode = LinearMode::new(storage, langbase, &config);
758
759        let history = vec![Thought::new("sess-1", "Only thought", "linear")];
760
761        let messages = mode.build_messages("Content", &history);
762
763        assert_eq!(messages.len(), 3);
764        assert!(messages[1].content.contains("Only thought"));
765    }
766
767    #[test]
768    fn test_build_messages_with_unicode_content() {
769        let config = create_test_config();
770        let rt = tokio::runtime::Runtime::new().unwrap();
771        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
772        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
773
774        let mode = LinearMode::new(storage, langbase, &config);
775
776        let unicode_content = "Hello ไธ–็•Œ ๐ŸŒ";
777        let messages = mode.build_messages(unicode_content, &[]);
778
779        assert_eq!(messages.len(), 2);
780        assert_eq!(messages[1].content, unicode_content);
781    }
782
783    #[test]
784    fn test_build_messages_with_multiline_content() {
785        let config = create_test_config();
786        let rt = tokio::runtime::Runtime::new().unwrap();
787        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
788        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
789
790        let mode = LinearMode::new(storage, langbase, &config);
791
792        let multiline = "Line 1\nLine 2\nLine 3";
793        let messages = mode.build_messages(multiline, &[]);
794
795        assert_eq!(messages.len(), 2);
796        assert_eq!(messages[1].content, multiline);
797        assert!(messages[1].content.contains('\n'));
798    }
799
800    #[test]
801    fn test_build_messages_with_special_characters() {
802        let config = create_test_config();
803        let rt = tokio::runtime::Runtime::new().unwrap();
804        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
805        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
806
807        let mode = LinearMode::new(storage, langbase, &config);
808
809        let special = "Test with: \n\t\r\"'\\{}[]()!@#$%^&*";
810        let messages = mode.build_messages(special, &[]);
811
812        assert_eq!(messages.len(), 2);
813        assert_eq!(messages[1].content, special);
814    }
815
816    #[test]
817    fn test_build_messages_history_formatting() {
818        let config = create_test_config();
819        let rt = tokio::runtime::Runtime::new().unwrap();
820        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
821        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
822
823        let mode = LinearMode::new(storage, langbase, &config);
824
825        let history = vec![
826            Thought::new("sess-1", "Thought A", "linear"),
827            Thought::new("sess-1", "Thought B", "linear"),
828            Thought::new("sess-1", "Thought C", "linear"),
829        ];
830
831        let messages = mode.build_messages("Query", &history);
832
833        // Check that history is formatted correctly with bullets
834        let history_msg = &messages[1].content;
835        assert!(history_msg.contains("- Thought A"));
836        assert!(history_msg.contains("- Thought B"));
837        assert!(history_msg.contains("- Thought C"));
838        assert!(history_msg.contains("Previous reasoning steps:"));
839        assert!(history_msg.contains("Now process this thought:"));
840    }
841
842    #[test]
843    fn test_build_messages_empty_content() {
844        let config = create_test_config();
845        let rt = tokio::runtime::Runtime::new().unwrap();
846        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
847        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
848
849        let mode = LinearMode::new(storage, langbase, &config);
850
851        let messages = mode.build_messages("", &[]);
852
853        assert_eq!(messages.len(), 2);
854        assert_eq!(messages[1].content, "");
855    }
856
857    #[test]
858    fn test_build_messages_whitespace_only() {
859        let config = create_test_config();
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 = LinearMode::new(storage, langbase, &config);
865
866        let whitespace = "   \t\n  ";
867        let messages = mode.build_messages(whitespace, &[]);
868
869        assert_eq!(messages.len(), 2);
870        assert_eq!(messages[1].content, whitespace);
871    }
872
873    #[test]
874    fn test_build_messages_very_long_content() {
875        let config = create_test_config();
876        let rt = tokio::runtime::Runtime::new().unwrap();
877        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
878        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
879
880        let mode = LinearMode::new(storage, langbase, &config);
881
882        let long_content = "x".repeat(10000);
883        let messages = mode.build_messages(&long_content, &[]);
884
885        assert_eq!(messages.len(), 2);
886        assert_eq!(messages[1].content.len(), 10000);
887    }
888
889    #[test]
890    fn test_build_messages_many_history_items() {
891        let config = create_test_config();
892        let rt = tokio::runtime::Runtime::new().unwrap();
893        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
894        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
895
896        let mode = LinearMode::new(storage, langbase, &config);
897
898        let history: Vec<Thought> = (0..50)
899            .map(|i| Thought::new("sess-1", format!("Thought {}", i), "linear"))
900            .collect();
901
902        let messages = mode.build_messages("Final query", &history);
903
904        assert_eq!(messages.len(), 3);
905        // Verify all thoughts are included
906        for i in 0..50 {
907            assert!(messages[1].content.contains(&format!("Thought {}", i)));
908        }
909    }
910
911    #[test]
912    fn test_build_messages_history_with_special_chars() {
913        let config = create_test_config();
914        let rt = tokio::runtime::Runtime::new().unwrap();
915        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
916        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
917
918        let mode = LinearMode::new(storage, langbase, &config);
919
920        let history = vec![
921            Thought::new("sess-1", "Thought with \"quotes\"", "linear"),
922            Thought::new("sess-1", "Thought with\nnewlines", "linear"),
923        ];
924
925        let messages = mode.build_messages("Query", &history);
926
927        assert_eq!(messages.len(), 3);
928        assert!(messages[1].content.contains("\"quotes\""));
929        assert!(messages[1].content.contains("newlines"));
930    }
931
932    // ============================================================================
933    // Clone and Debug Trait Tests
934    // ============================================================================
935
936    #[test]
937    fn test_linear_params_clone_trait() {
938        let original = LinearParams::new("Original")
939            .with_session("sess-1")
940            .with_confidence(0.85);
941
942        let cloned = original.clone();
943
944        assert_eq!(original.content, cloned.content);
945        assert_eq!(original.session_id, cloned.session_id);
946        assert_eq!(original.confidence, cloned.confidence);
947    }
948
949    #[test]
950    fn test_linear_params_debug_trait() {
951        let params = LinearParams::new("Debug test")
952            .with_session("sess-123")
953            .with_confidence(0.9);
954
955        let debug_str = format!("{:?}", params);
956
957        assert!(debug_str.contains("LinearParams"));
958        assert!(debug_str.contains("Debug test"));
959        assert!(debug_str.contains("sess-123"));
960        assert!(debug_str.contains("0.9"));
961    }
962
963    #[test]
964    fn test_linear_result_clone_trait() {
965        let original = LinearResult {
966            thought_id: "t-1".to_string(),
967            session_id: "s-1".to_string(),
968            content: "Content".to_string(),
969            confidence: 0.88,
970            previous_thought: Some("t-0".to_string()),
971        };
972
973        let cloned = original.clone();
974
975        assert_eq!(original.thought_id, cloned.thought_id);
976        assert_eq!(original.session_id, cloned.session_id);
977        assert_eq!(original.content, cloned.content);
978        assert_eq!(original.confidence, cloned.confidence);
979        assert_eq!(original.previous_thought, cloned.previous_thought);
980    }
981
982    #[test]
983    fn test_linear_result_debug_trait() {
984        let result = LinearResult {
985            thought_id: "t-123".to_string(),
986            session_id: "s-456".to_string(),
987            content: "Debug result".to_string(),
988            confidence: 0.92,
989            previous_thought: None,
990        };
991
992        let debug_str = format!("{:?}", result);
993
994        assert!(debug_str.contains("LinearResult"));
995        assert!(debug_str.contains("t-123"));
996        assert!(debug_str.contains("s-456"));
997        assert!(debug_str.contains("Debug result"));
998    }
999
1000    // ============================================================================
1001    // Message Role Tests (from langbase module)
1002    // ============================================================================
1003
1004    #[test]
1005    fn test_message_roles_in_build_messages() {
1006        let config = create_test_config();
1007        let rt = tokio::runtime::Runtime::new().unwrap();
1008        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
1009        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
1010
1011        let mode = LinearMode::new(storage, langbase, &config);
1012        let messages = mode.build_messages("Test", &[]);
1013
1014        // First message should be system
1015        assert!(matches!(messages[0].role, MessageRole::System));
1016        // Second message should be user
1017        assert!(matches!(messages[1].role, MessageRole::User));
1018    }
1019
1020    #[test]
1021    fn test_message_roles_with_history() {
1022        let config = create_test_config();
1023        let rt = tokio::runtime::Runtime::new().unwrap();
1024        let storage = rt.block_on(SqliteStorage::new_in_memory()).unwrap();
1025        let langbase = LangbaseClient::new(&config.langbase, RequestConfig::default()).unwrap();
1026
1027        let mode = LinearMode::new(storage, langbase, &config);
1028        let history = vec![Thought::new("sess-1", "Previous", "linear")];
1029        let messages = mode.build_messages("Current", &history);
1030
1031        // System, history user message, current user message
1032        assert!(matches!(messages[0].role, MessageRole::System));
1033        assert!(matches!(messages[1].role, MessageRole::User));
1034        assert!(matches!(messages[2].role, MessageRole::User));
1035    }
1036
1037    // ============================================================================
1038    // Additional Edge Cases
1039    // ============================================================================
1040
1041    #[test]
1042    fn test_reasoning_response_confidence_bounds() {
1043        // Test valid confidence values
1044        let json_low = r#"{"thought": "Low confidence", "confidence": 0.0, "metadata": null}"#;
1045        let response_low = ReasoningResponse::from_completion(json_low);
1046        assert_eq!(response_low.confidence, 0.0);
1047
1048        let json_high = r#"{"thought": "High confidence", "confidence": 1.0, "metadata": null}"#;
1049        let response_high = ReasoningResponse::from_completion(json_high);
1050        assert_eq!(response_high.confidence, 1.0);
1051    }
1052
1053    #[test]
1054    fn test_reasoning_response_multiline_thought() {
1055        let json = r#"{"thought": "Line 1\nLine 2\nLine 3", "confidence": 0.85, "metadata": null}"#;
1056        let response = ReasoningResponse::from_completion(json);
1057        assert!(response.thought.contains("Line 1"));
1058        assert!(response.thought.contains("Line 2"));
1059        assert!(response.thought.contains("Line 3"));
1060        assert_eq!(response.confidence, 0.85);
1061    }
1062
1063    #[test]
1064    fn test_reasoning_response_empty_thought() {
1065        let json = r#"{"thought": "", "confidence": 0.5, "metadata": null}"#;
1066        let response = ReasoningResponse::from_completion(json);
1067        assert_eq!(response.thought, "");
1068        assert_eq!(response.confidence, 0.5);
1069    }
1070
1071    #[test]
1072    fn test_reasoning_response_very_long_thought() {
1073        let long_thought = "a".repeat(10000);
1074        let json = format!(
1075            r#"{{"thought": "{}", "confidence": 0.8, "metadata": null}}"#,
1076            long_thought
1077        );
1078        let response = ReasoningResponse::from_completion(&json);
1079        assert_eq!(response.thought.len(), 10000);
1080        assert_eq!(response.confidence, 0.8);
1081    }
1082
1083    #[test]
1084    fn test_default_confidence_function() {
1085        // Test that the default confidence function is consistent
1086        assert_eq!(default_confidence(), 0.8);
1087        assert_eq!(default_confidence(), default_confidence());
1088    }
1089
1090    #[test]
1091    fn test_linear_params_new_from_string() {
1092        let s = String::from("Test string");
1093        let params = LinearParams::new(s);
1094        assert_eq!(params.content, "Test string");
1095    }
1096
1097    #[test]
1098    fn test_linear_params_new_from_str() {
1099        let params = LinearParams::new("String slice");
1100        assert_eq!(params.content, "String slice");
1101    }
1102
1103    #[test]
1104    fn test_linear_params_confidence_precision() {
1105        // Test precise confidence values
1106        let params1 = LinearParams::new("Test").with_confidence(0.123456789);
1107        assert_eq!(params1.confidence, 0.123456789);
1108
1109        let params2 = LinearParams::new("Test").with_confidence(0.999999999);
1110        assert_eq!(params2.confidence, 0.999999999);
1111    }
1112
1113    #[test]
1114    fn test_linear_result_all_fields() {
1115        // Test that all fields are properly stored
1116        let result = LinearResult {
1117            thought_id: "id-1".to_string(),
1118            session_id: "sid-2".to_string(),
1119            content: "Test content".to_string(),
1120            confidence: 0.777,
1121            previous_thought: Some("prev-id".to_string()),
1122        };
1123
1124        assert_eq!(result.thought_id, "id-1");
1125        assert_eq!(result.session_id, "sid-2");
1126        assert_eq!(result.content, "Test content");
1127        assert_eq!(result.confidence, 0.777);
1128        assert_eq!(result.previous_thought, Some("prev-id".to_string()));
1129    }
1130
1131    #[test]
1132    fn test_linear_params_session_none_by_default() {
1133        let params = LinearParams::new("Test");
1134        assert!(params.session_id.is_none());
1135    }
1136
1137    #[test]
1138    fn test_linear_params_confidence_default_value() {
1139        let params = LinearParams::new("Test");
1140        assert_eq!(params.confidence, 0.8);
1141    }
1142}