1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct LinearParams {
22 pub content: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub session_id: Option<String>,
27 #[serde(default = "default_confidence")]
29 pub confidence: f64,
30}
31
32fn default_confidence() -> f64 {
33 0.8
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LinearResult {
39 pub thought_id: String,
41 pub session_id: String,
43 pub content: String,
45 pub confidence: f64,
47 pub previous_thought: Option<String>,
49}
50
51#[derive(Clone)]
53pub struct LinearMode {
54 core: ModeCore,
56 pipe_name: String,
58}
59
60impl LinearMode {
61 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 pub async fn process(&self, params: LinearParams) -> AppResult<LinearResult> {
71 let start = Instant::now();
72
73 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 let session = self
84 .core
85 .storage()
86 .get_or_create_session(¶ms.session_id, "linear")
87 .await?;
88
89 debug!(session_id = %session.id, "Processing linear reasoning");
90
91 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 let messages = self.build_messages(¶ms.content, &previous_thoughts);
101
102 let mut invocation = Invocation::new(
104 "reasoning.linear",
105 serialize_for_log(¶ms, "reasoning.linear input"),
106 )
107 .with_session(&session.id)
108 .with_pipe(&self.pipe_name);
109
110 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 let reasoning = ReasoningResponse::from_completion(&response.completion);
124
125 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 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 fn build_messages(&self, content: &str, history: &[Thought]) -> Vec<Message> {
157 let mut messages = Vec::new();
158
159 messages.push(Message::system(LINEAR_REASONING_PROMPT));
161
162 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 messages.push(Message::user(content.to_string()));
175
176 messages
177 }
178}
179
180impl LinearParams {
181 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 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 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 #[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(¶ms).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); }
287
288 #[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 #[test]
347 fn test_default_confidence() {
348 assert_eq!(default_confidence(), 0.8);
349 }
350
351 #[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 #[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 #[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 #[test]
517 fn test_linear_params_skip_none_session() {
518 let params = LinearParams::new("Test");
519 let json = serde_json::to_string(¶ms).unwrap();
520 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 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 assert_eq!(parsed["previous_thought"], serde_json::Value::Null);
589 }
590
591 #[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 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 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 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 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 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 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 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 #[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 #[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 assert!(matches!(messages[0].role, MessageRole::System));
1016 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 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 #[test]
1042 fn test_reasoning_response_confidence_bounds() {
1043 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 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 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 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}