1use std::future::Future;
5use std::pin::Pin;
6use std::{
7 any::TypeId,
8 collections::HashMap,
9 sync::{LazyLock, Mutex},
10};
11
12use futures_core::Stream;
13use serde::{Deserialize, Serialize};
14
15use zeph_common::ToolName;
16
17pub use zeph_common::ToolDefinition;
18
19use crate::embed::owned_strs;
20use crate::error::LlmError;
21
22static SCHEMA_CACHE: LazyLock<Mutex<HashMap<TypeId, (serde_json::Value, String)>>> =
23 LazyLock::new(|| Mutex::new(HashMap::new()));
24
25pub(crate) fn cached_schema<T: schemars::JsonSchema + 'static>()
31-> Result<(serde_json::Value, String), crate::LlmError> {
32 let type_id = TypeId::of::<T>();
33 if let Ok(cache) = SCHEMA_CACHE.lock()
34 && let Some(entry) = cache.get(&type_id)
35 {
36 return Ok(entry.clone());
37 }
38 let schema = schemars::schema_for!(T);
39 let value = serde_json::to_value(&schema)
40 .map_err(|e| crate::LlmError::StructuredParse(e.to_string()))?;
41 let pretty = serde_json::to_string_pretty(&schema)
42 .map_err(|e| crate::LlmError::StructuredParse(e.to_string()))?;
43 if let Ok(mut cache) = SCHEMA_CACHE.lock() {
44 cache.insert(type_id, (value.clone(), pretty.clone()));
45 }
46 Ok((value, pretty))
47}
48
49pub(crate) fn short_type_name<T: ?Sized>() -> &'static str {
63 std::any::type_name::<T>()
64 .rsplit("::")
65 .next()
66 .unwrap_or("Output")
67}
68
69#[non_exhaustive]
78#[derive(Debug, Clone, Default)]
79pub struct ChatExtras {
80 pub entropy: Option<f64>,
85}
86
87impl ChatExtras {
88 #[must_use]
101 pub fn with_entropy(entropy: f64) -> Self {
102 Self {
103 entropy: Some(entropy),
104 }
105 }
106}
107
108#[non_exhaustive]
113#[derive(Debug, Clone)]
114pub enum StreamChunk {
115 Content(String),
117 Thinking(String),
119 Compaction(String),
122 ToolUse(Vec<ToolUseRequest>),
124}
125
126pub type ChatStream = Pin<Box<dyn Stream<Item = Result<StreamChunk, LlmError>> + Send>>;
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ToolUseRequest {
139 pub id: String,
141 pub name: ToolName,
143 pub input: serde_json::Value,
145}
146
147#[non_exhaustive]
153#[derive(Debug, Clone)]
154pub enum ThinkingBlock {
155 Thinking { thinking: String, signature: String },
157 Redacted { data: String },
159}
160
161pub const MAX_TOKENS_TRUNCATION_MARKER: &str = "max_tokens limit reached";
164
165#[non_exhaustive]
173#[derive(Debug, Clone)]
174pub enum ChatResponse {
175 Text(String),
177 ToolUse {
179 text: Option<String>,
181 tool_calls: Vec<ToolUseRequest>,
182 thinking_blocks: Vec<ThinkingBlock>,
185 },
186}
187
188pub type EmbedFuture = Pin<Box<dyn Future<Output = Result<Vec<f32>, LlmError>> + Send>>;
190
191pub type EmbedFn = Box<dyn Fn(&str) -> EmbedFuture + Send + Sync>;
196
197pub type StatusTx = tokio::sync::mpsc::UnboundedSender<String>;
203
204#[must_use]
207pub fn default_debug_request_json(
208 messages: &[Message],
209 tools: &[ToolDefinition],
210) -> serde_json::Value {
211 serde_json::json!({
212 "model": serde_json::Value::Null,
213 "max_tokens": serde_json::Value::Null,
214 "messages": serde_json::to_value(messages).unwrap_or(serde_json::Value::Array(vec![])),
215 "tools": serde_json::to_value(tools).unwrap_or(serde_json::Value::Array(vec![])),
216 "temperature": serde_json::Value::Null,
217 "cache_control": serde_json::Value::Null,
218 })
219}
220
221#[derive(Debug, Clone, Default)]
230pub struct GenerationOverrides {
231 pub temperature: Option<f64>,
233 pub top_p: Option<f64>,
235 pub top_k: Option<usize>,
237 pub frequency_penalty: Option<f64>,
239 pub presence_penalty: Option<f64>,
241}
242
243#[non_exhaustive]
250#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "lowercase")]
252pub enum Role {
253 System,
254 User,
255 Assistant,
256}
257
258#[non_exhaustive]
273#[derive(Clone, Debug, Serialize, Deserialize)]
274#[serde(tag = "kind", rename_all = "snake_case")]
275pub enum MessagePart {
276 Text { text: String },
278 ToolOutput {
280 tool_name: zeph_common::ToolName,
281 body: String,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 compacted_at: Option<i64>,
284 },
285 Recall { text: String },
287 CodeContext { text: String },
289 Summary { text: String },
291 CrossSession { text: String },
293 ToolUse {
295 id: String,
296 name: String,
297 input: serde_json::Value,
298 },
299 ToolResult {
301 tool_use_id: String,
302 content: String,
303 #[serde(default)]
304 is_error: bool,
305 },
306 Image(Box<ImageData>),
308 ThinkingBlock { thinking: String, signature: String },
310 RedactedThinkingBlock { data: String },
312 Compaction { summary: String },
315}
316
317impl MessagePart {
318 #[must_use]
321 pub fn as_plain_text(&self) -> Option<&str> {
322 match self {
323 Self::Text { text }
324 | Self::Recall { text }
325 | Self::CodeContext { text }
326 | Self::Summary { text }
327 | Self::CrossSession { text } => Some(text.as_str()),
328 _ => None,
329 }
330 }
331
332 #[must_use]
334 pub fn as_image(&self) -> Option<&ImageData> {
335 if let Self::Image(img) = self {
336 Some(img)
337 } else {
338 None
339 }
340 }
341
342 #[must_use]
369 pub fn strip_images(parts: &[MessagePart]) -> Vec<MessagePart> {
370 parts
371 .iter()
372 .filter(|p| !matches!(p, MessagePart::Image(_)))
373 .cloned()
374 .collect()
375 }
376}
377
378#[derive(Clone, Serialize, Deserialize)]
379pub struct ImageData {
384 #[serde(with = "serde_bytes_base64")]
385 pub data: Vec<u8>,
386 pub mime_type: String,
387}
388
389impl std::fmt::Debug for ImageData {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 write!(f, "[image: {}, {} bytes]", self.mime_type, self.data.len())
396 }
397}
398
399mod serde_bytes_base64 {
400 use base64::{Engine, engine::general_purpose::STANDARD};
401 use serde::{Deserialize, Deserializer, Serializer};
402
403 pub fn serialize<S>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error>
404 where
405 S: Serializer,
406 {
407 s.serialize_str(&STANDARD.encode(bytes))
408 }
409
410 pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
411 where
412 D: Deserializer<'de>,
413 {
414 let s = String::deserialize(d)?;
415 STANDARD.decode(&s).map_err(serde::de::Error::custom)
416 }
417}
418
419#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "snake_case")]
436#[non_exhaustive]
437pub enum MessageVisibility {
438 Both,
440 AgentOnly,
442 UserOnly,
444}
445
446impl MessageVisibility {
447 #[must_use]
449 pub fn is_agent_visible(self) -> bool {
450 matches!(self, MessageVisibility::Both | MessageVisibility::AgentOnly)
451 }
452
453 #[must_use]
455 pub fn is_user_visible(self) -> bool {
456 matches!(self, MessageVisibility::Both | MessageVisibility::UserOnly)
457 }
458}
459
460impl Default for MessageVisibility {
461 fn default() -> Self {
463 MessageVisibility::Both
464 }
465}
466
467impl MessageVisibility {
468 #[must_use]
470 pub fn as_db_str(self) -> &'static str {
471 match self {
472 MessageVisibility::Both => "both",
473 MessageVisibility::AgentOnly => "agent_only",
474 MessageVisibility::UserOnly => "user_only",
475 }
476 }
477
478 #[must_use]
482 pub fn from_db_str(s: &str) -> Self {
483 match s {
484 "agent_only" => MessageVisibility::AgentOnly,
485 "user_only" => MessageVisibility::UserOnly,
486 _ => MessageVisibility::Both,
487 }
488 }
489}
490
491#[derive(Clone, Debug, Serialize, Deserialize)]
496pub struct MessageMetadata {
497 pub visibility: MessageVisibility,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub compacted_at: Option<i64>,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
505 pub deferred_summary: Option<String>,
506 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
509 pub focus_pinned: bool,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub focus_marker_id: Option<uuid::Uuid>,
514 #[serde(skip)]
517 pub db_id: Option<i64>,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub fidelity_tag: Option<zeph_common::ContextFidelity>,
524 #[serde(skip)]
528 pub embedding: Option<Vec<f32>>,
529}
530
531impl Default for MessageMetadata {
532 fn default() -> Self {
533 Self {
534 visibility: MessageVisibility::Both,
535 compacted_at: None,
536 deferred_summary: None,
537 focus_pinned: false,
538 focus_marker_id: None,
539 db_id: None,
540 fidelity_tag: None,
541 embedding: None,
542 }
543 }
544}
545
546impl MessageMetadata {
547 #[must_use]
549 pub fn agent_only() -> Self {
550 Self {
551 visibility: MessageVisibility::AgentOnly,
552 compacted_at: None,
553 deferred_summary: None,
554 focus_pinned: false,
555 focus_marker_id: None,
556 db_id: None,
557 fidelity_tag: None,
558 embedding: None,
559 }
560 }
561
562 #[must_use]
564 pub fn user_only() -> Self {
565 Self {
566 visibility: MessageVisibility::UserOnly,
567 compacted_at: None,
568 deferred_summary: None,
569 focus_pinned: false,
570 focus_marker_id: None,
571 db_id: None,
572 fidelity_tag: None,
573 embedding: None,
574 }
575 }
576
577 #[must_use]
579 pub fn focus_pinned() -> Self {
580 Self {
581 visibility: MessageVisibility::AgentOnly,
582 compacted_at: None,
583 deferred_summary: None,
584 focus_pinned: true,
585 focus_marker_id: None,
586 db_id: None,
587 fidelity_tag: None,
588 embedding: None,
589 }
590 }
591}
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
620pub struct Message {
621 pub role: Role,
622 pub content: String,
624 #[serde(default)]
625 pub parts: Vec<MessagePart>,
626 #[serde(default)]
627 pub metadata: MessageMetadata,
628}
629
630impl Default for Message {
631 fn default() -> Self {
632 Self {
633 role: Role::User,
634 content: String::new(),
635 parts: vec![],
636 metadata: MessageMetadata::default(),
637 }
638 }
639}
640
641impl Message {
642 #[must_use]
647 pub fn from_legacy(role: Role, content: impl Into<String>) -> Self {
648 Self {
649 role,
650 content: content.into(),
651 parts: vec![],
652 metadata: MessageMetadata::default(),
653 }
654 }
655
656 #[must_use]
661 pub fn from_parts(role: Role, parts: Vec<MessagePart>) -> Self {
662 let content = Self::flatten_parts(&parts);
663 Self {
664 role,
665 content,
666 parts,
667 metadata: MessageMetadata::default(),
668 }
669 }
670
671 #[must_use]
674 pub fn to_llm_content(&self) -> &str {
675 &self.content
676 }
677
678 pub fn rebuild_content(&mut self) {
680 if !self.parts.is_empty() {
681 self.content = Self::flatten_parts(&self.parts);
682 }
683 }
684
685 fn flatten_parts(parts: &[MessagePart]) -> String {
686 use std::fmt::Write;
687 let mut out = String::new();
688 for part in parts {
689 match part {
690 MessagePart::Text { text }
691 | MessagePart::Recall { text }
692 | MessagePart::CodeContext { text }
693 | MessagePart::Summary { text }
694 | MessagePart::CrossSession { text } => out.push_str(text),
695 MessagePart::ToolOutput {
696 tool_name,
697 body,
698 compacted_at,
699 } => {
700 if compacted_at.is_some() {
701 if body.is_empty() {
702 let _ = write!(out, "[tool output: {tool_name}] (pruned)");
703 } else {
704 let _ = write!(out, "[tool output: {tool_name}] {body}");
705 }
706 } else {
707 let _ = write!(out, "[tool output: {tool_name}]\n```\n{body}\n```");
708 }
709 }
710 MessagePart::ToolUse { id, name, .. } => {
711 let _ = write!(out, "[tool_use: {name}({id})]");
712 }
713 MessagePart::ToolResult {
714 tool_use_id,
715 content,
716 ..
717 } => {
718 let _ = write!(out, "[tool_result: {tool_use_id}]\n{content}");
719 }
720 MessagePart::Image(img) => {
721 let _ = write!(out, "[image: {}, {} bytes]", img.mime_type, img.data.len());
722 }
723 MessagePart::ThinkingBlock { .. }
725 | MessagePart::RedactedThinkingBlock { .. }
726 | MessagePart::Compaction { .. } => {}
727 }
728 }
729 out
730 }
731}
732
733pub trait LlmProvider: Send + Sync {
803 fn context_window(&self) -> Option<usize> {
807 None
808 }
809
810 fn chat(&self, messages: &[Message]) -> impl Future<Output = Result<String, LlmError>> + Send;
816
817 fn chat_stream(
823 &self,
824 messages: &[Message],
825 ) -> impl Future<Output = Result<ChatStream, LlmError>> + Send;
826
827 fn supports_streaming(&self) -> bool;
829
830 fn embed(&self, text: &str) -> impl Future<Output = Result<Vec<f32>, LlmError>> + Send;
836
837 fn embed_batch(
847 &self,
848 texts: &[&str],
849 ) -> impl Future<Output = Result<Vec<Vec<f32>>, LlmError>> + Send {
850 let owned = owned_strs(texts);
851 async move {
852 let mut results = Vec::with_capacity(owned.len());
853 for text in &owned {
854 results.push(self.embed(text).await?);
855 }
856 Ok(results)
857 }
858 }
859
860 fn supports_embeddings(&self) -> bool;
862
863 fn name(&self) -> &str;
865
866 #[allow(clippy::unnecessary_literal_bound)]
869 fn model_identifier(&self) -> &str {
870 ""
871 }
872
873 fn effective_model_identifier(&self) -> &str {
885 self.model_identifier()
886 }
887
888 fn supports_vision(&self) -> bool {
890 false
891 }
892
893 fn supports_tool_use(&self) -> bool {
900 false
901 }
902
903 fn chat_with_tools(
911 &self,
912 messages: &[Message],
913 _tools: &[ToolDefinition],
914 ) -> impl std::future::Future<Output = Result<ChatResponse, LlmError>> + Send {
915 let msgs = messages.to_vec();
916 async move { Ok(ChatResponse::Text(self.chat(&msgs).await?)) }
917 }
918
919 fn last_cache_usage(&self) -> Option<(u64, u64)> {
922 None
923 }
924
925 fn last_usage(&self) -> Option<(u64, u64)> {
928 None
929 }
930
931 fn last_reasoning_tokens(&self) -> Option<u64> {
936 None
937 }
938
939 fn take_compaction_summary(&self) -> Option<String> {
942 None
943 }
944
945 fn chat_with_extras(
960 &self,
961 messages: &[Message],
962 ) -> impl Future<Output = Result<(String, ChatExtras), LlmError>> + Send {
963 let msgs = messages.to_vec();
964 async move { Ok((self.chat(&msgs).await?, ChatExtras::default())) }
965 }
966
967 #[must_use]
971 fn debug_request_json(
972 &self,
973 messages: &[Message],
974 tools: &[ToolDefinition],
975 _stream: bool,
976 ) -> serde_json::Value {
977 default_debug_request_json(messages, tools)
978 }
979
980 fn list_models(&self) -> Vec<String> {
983 vec![]
984 }
985
986 fn supports_structured_output(&self) -> bool {
988 false
989 }
990
991 #[allow(async_fn_in_trait)]
1002 async fn chat_typed<T>(&self, messages: &[Message]) -> Result<T, LlmError>
1003 where
1004 T: serde::de::DeserializeOwned + schemars::JsonSchema + 'static,
1005 Self: Sized,
1006 {
1007 let (_, schema_json) = cached_schema::<T>()?;
1008 let type_name = short_type_name::<T>();
1009
1010 let mut augmented = messages.to_vec();
1011 let instruction = format!(
1012 "Respond with a valid JSON object matching this schema. \
1013 Output ONLY the JSON, no markdown fences or extra text.\n\n\
1014 Type: {type_name}\nSchema:\n```json\n{schema_json}\n```"
1015 );
1016 augmented.insert(0, Message::from_legacy(Role::System, instruction));
1017
1018 let raw = self.chat(&augmented).await?;
1019 let cleaned = strip_json_fences(&raw);
1020 match serde_json::from_str::<T>(cleaned) {
1021 Ok(val) => Ok(val),
1022 Err(first_err) => {
1023 augmented.push(Message::from_legacy(Role::Assistant, &raw));
1024 augmented.push(Message::from_legacy(
1025 Role::User,
1026 format!(
1027 "Your response was not valid JSON. Error: {first_err}. \
1028 Please output ONLY valid JSON matching the schema."
1029 ),
1030 ));
1031 let retry_raw = self.chat(&augmented).await?;
1032 let retry_cleaned = strip_json_fences(&retry_raw);
1033 serde_json::from_str::<T>(retry_cleaned).map_err(|e| {
1034 LlmError::StructuredParse(format!("parse failed after retry: {e}"))
1035 })
1036 }
1037 }
1038 }
1039}
1040
1041fn strip_json_fences(s: &str) -> &str {
1045 s.trim()
1046 .trim_start_matches("```json")
1047 .trim_start_matches("```")
1048 .trim_end_matches("```")
1049 .trim()
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use std::assert_matches;
1055 use tokio_stream::StreamExt;
1056
1057 use super::*;
1058
1059 struct StubProvider {
1060 response: String,
1061 }
1062
1063 impl LlmProvider for StubProvider {
1064 async fn chat(&self, _messages: &[Message]) -> Result<String, LlmError> {
1065 Ok(self.response.clone())
1066 }
1067
1068 async fn chat_stream(&self, messages: &[Message]) -> Result<ChatStream, LlmError> {
1069 let response = self.chat(messages).await?;
1070 Ok(Box::pin(tokio_stream::once(Ok(StreamChunk::Content(
1071 response,
1072 )))))
1073 }
1074
1075 fn supports_streaming(&self) -> bool {
1076 false
1077 }
1078
1079 async fn embed(&self, _text: &str) -> Result<Vec<f32>, LlmError> {
1080 Ok(vec![0.1, 0.2, 0.3])
1081 }
1082
1083 fn supports_embeddings(&self) -> bool {
1084 false
1085 }
1086
1087 fn name(&self) -> &'static str {
1088 "stub"
1089 }
1090 }
1091
1092 #[test]
1093 fn test_image_data_debug_redacts_bytes() {
1094 let img = ImageData {
1095 data: vec![0xAB, 0xCD, 0xEF],
1096 mime_type: "image/png".to_owned(),
1097 };
1098 let debug = format!("{img:?}");
1099 assert_eq!(debug, "[image: image/png, 3 bytes]");
1100 assert!(!debug.contains("171") && !debug.contains("205") && !debug.contains("239"));
1101 }
1102
1103 #[test]
1104 fn context_window_default_returns_none() {
1105 let provider = StubProvider {
1106 response: String::new(),
1107 };
1108 assert!(provider.context_window().is_none());
1109 }
1110
1111 #[test]
1112 fn supports_streaming_default_returns_false() {
1113 let provider = StubProvider {
1114 response: String::new(),
1115 };
1116 assert!(!provider.supports_streaming());
1117 }
1118
1119 #[test]
1120 fn supports_tool_use_default_returns_false() {
1121 let provider = StubProvider {
1127 response: String::new(),
1128 };
1129 assert!(!provider.supports_tool_use());
1130 }
1131
1132 #[tokio::test]
1133 async fn chat_stream_default_yields_single_chunk() {
1134 let provider = StubProvider {
1135 response: "hello world".into(),
1136 };
1137 let messages = vec![Message {
1138 role: Role::User,
1139 content: "test".into(),
1140 parts: vec![],
1141 metadata: MessageMetadata::default(),
1142 }];
1143
1144 let mut stream = provider.chat_stream(&messages).await.unwrap();
1145 let chunk = stream.next().await.unwrap().unwrap();
1146 assert_matches!(chunk, StreamChunk::Content(s) if s == "hello world");
1147 assert!(stream.next().await.is_none());
1148 }
1149
1150 #[tokio::test]
1151 async fn chat_stream_default_propagates_chat_error() {
1152 struct FailProvider;
1153
1154 impl LlmProvider for FailProvider {
1155 async fn chat(&self, _messages: &[Message]) -> Result<String, LlmError> {
1156 Err(LlmError::Unavailable)
1157 }
1158
1159 async fn chat_stream(&self, messages: &[Message]) -> Result<ChatStream, LlmError> {
1160 let response = self.chat(messages).await?;
1161 Ok(Box::pin(tokio_stream::once(Ok(StreamChunk::Content(
1162 response,
1163 )))))
1164 }
1165
1166 fn supports_streaming(&self) -> bool {
1167 false
1168 }
1169
1170 async fn embed(&self, _text: &str) -> Result<Vec<f32>, LlmError> {
1171 Err(LlmError::Unavailable)
1172 }
1173
1174 fn supports_embeddings(&self) -> bool {
1175 false
1176 }
1177
1178 fn name(&self) -> &'static str {
1179 "fail"
1180 }
1181 }
1182
1183 let provider = FailProvider;
1184 let messages = vec![Message {
1185 role: Role::User,
1186 content: "test".into(),
1187 parts: vec![],
1188 metadata: MessageMetadata::default(),
1189 }];
1190
1191 let result = provider.chat_stream(&messages).await;
1192 assert!(result.is_err());
1193 if let Err(e) = result {
1194 assert!(e.to_string().contains("provider unavailable"));
1195 }
1196 }
1197
1198 #[tokio::test]
1199 async fn stub_provider_embed_returns_vector() {
1200 let provider = StubProvider {
1201 response: String::new(),
1202 };
1203 let embedding = provider.embed("test").await.unwrap();
1204 assert_eq!(embedding, vec![0.1, 0.2, 0.3]);
1205 }
1206
1207 #[tokio::test]
1208 async fn fail_provider_embed_propagates_error() {
1209 struct FailProvider;
1210
1211 impl LlmProvider for FailProvider {
1212 async fn chat(&self, _messages: &[Message]) -> Result<String, LlmError> {
1213 Err(LlmError::Unavailable)
1214 }
1215
1216 async fn chat_stream(&self, messages: &[Message]) -> Result<ChatStream, LlmError> {
1217 let response = self.chat(messages).await?;
1218 Ok(Box::pin(tokio_stream::once(Ok(StreamChunk::Content(
1219 response,
1220 )))))
1221 }
1222
1223 fn supports_streaming(&self) -> bool {
1224 false
1225 }
1226
1227 async fn embed(&self, _text: &str) -> Result<Vec<f32>, LlmError> {
1228 Err(LlmError::EmbedUnsupported {
1229 provider: "fail".into(),
1230 })
1231 }
1232
1233 fn supports_embeddings(&self) -> bool {
1234 false
1235 }
1236
1237 fn name(&self) -> &'static str {
1238 "fail"
1239 }
1240 }
1241
1242 let provider = FailProvider;
1243 let result = provider.embed("test").await;
1244 assert!(result.is_err());
1245 assert!(
1246 result
1247 .unwrap_err()
1248 .to_string()
1249 .contains("embedding not supported")
1250 );
1251 }
1252
1253 #[test]
1254 fn role_serialization() {
1255 let system = Role::System;
1256 let user = Role::User;
1257 let assistant = Role::Assistant;
1258
1259 assert_eq!(serde_json::to_string(&system).unwrap(), "\"system\"");
1260 assert_eq!(serde_json::to_string(&user).unwrap(), "\"user\"");
1261 assert_eq!(serde_json::to_string(&assistant).unwrap(), "\"assistant\"");
1262 }
1263
1264 #[test]
1265 fn role_deserialization() {
1266 let system: Role = serde_json::from_str("\"system\"").unwrap();
1267 let user: Role = serde_json::from_str("\"user\"").unwrap();
1268 let assistant: Role = serde_json::from_str("\"assistant\"").unwrap();
1269
1270 assert_eq!(system, Role::System);
1271 assert_eq!(user, Role::User);
1272 assert_eq!(assistant, Role::Assistant);
1273 }
1274
1275 #[test]
1276 fn message_clone() {
1277 let msg = Message {
1278 role: Role::User,
1279 content: "test".into(),
1280 parts: vec![],
1281 metadata: MessageMetadata::default(),
1282 };
1283 let cloned = msg.clone();
1284 assert_eq!(cloned.role, msg.role);
1285 assert_eq!(cloned.content, msg.content);
1286 }
1287
1288 #[test]
1289 fn message_debug() {
1290 let msg = Message {
1291 role: Role::Assistant,
1292 content: "response".into(),
1293 parts: vec![],
1294 metadata: MessageMetadata::default(),
1295 };
1296 let debug = format!("{msg:?}");
1297 assert!(debug.contains("Assistant"));
1298 assert!(debug.contains("response"));
1299 }
1300
1301 #[test]
1302 fn message_serialization() {
1303 let msg = Message {
1304 role: Role::User,
1305 content: "hello".into(),
1306 parts: vec![],
1307 metadata: MessageMetadata::default(),
1308 };
1309 let json = serde_json::to_string(&msg).unwrap();
1310 assert!(json.contains("\"role\":\"user\""));
1311 assert!(json.contains("\"content\":\"hello\""));
1312 }
1313
1314 #[test]
1315 fn message_part_serde_round_trip() {
1316 let parts = vec![
1317 MessagePart::Text {
1318 text: "hello".into(),
1319 },
1320 MessagePart::ToolOutput {
1321 tool_name: "bash".into(),
1322 body: "output".into(),
1323 compacted_at: None,
1324 },
1325 MessagePart::Recall {
1326 text: "recall".into(),
1327 },
1328 MessagePart::CodeContext {
1329 text: "code".into(),
1330 },
1331 MessagePart::Summary {
1332 text: "summary".into(),
1333 },
1334 ];
1335 let json = serde_json::to_string(&parts).unwrap();
1336 let deserialized: Vec<MessagePart> = serde_json::from_str(&json).unwrap();
1337 assert_eq!(deserialized.len(), 5);
1338 }
1339
1340 #[test]
1341 fn from_legacy_creates_empty_parts() {
1342 let msg = Message::from_legacy(Role::User, "hello");
1343 assert_eq!(msg.role, Role::User);
1344 assert_eq!(msg.content, "hello");
1345 assert!(msg.parts.is_empty());
1346 assert_eq!(msg.to_llm_content(), "hello");
1347 }
1348
1349 #[test]
1350 fn from_parts_flattens_content() {
1351 let msg = Message::from_parts(
1352 Role::System,
1353 vec![MessagePart::Recall {
1354 text: "recalled data".into(),
1355 }],
1356 );
1357 assert_eq!(msg.content, "recalled data");
1358 assert_eq!(msg.to_llm_content(), "recalled data");
1359 assert_eq!(msg.parts.len(), 1);
1360 }
1361
1362 #[test]
1363 fn from_parts_tool_output_format() {
1364 let msg = Message::from_parts(
1365 Role::User,
1366 vec![MessagePart::ToolOutput {
1367 tool_name: "bash".into(),
1368 body: "hello world".into(),
1369 compacted_at: None,
1370 }],
1371 );
1372 assert!(msg.content.contains("[tool output: bash]"));
1373 assert!(msg.content.contains("hello world"));
1374 }
1375
1376 #[test]
1377 fn message_deserializes_without_parts() {
1378 let json = r#"{"role":"user","content":"hello"}"#;
1379 let msg: Message = serde_json::from_str(json).unwrap();
1380 assert_eq!(msg.content, "hello");
1381 assert!(msg.parts.is_empty());
1382 }
1383
1384 #[test]
1385 fn flatten_skips_compacted_tool_output_empty_body() {
1386 let msg = Message::from_parts(
1388 Role::User,
1389 vec![
1390 MessagePart::Text {
1391 text: "prefix ".into(),
1392 },
1393 MessagePart::ToolOutput {
1394 tool_name: "bash".into(),
1395 body: String::new(),
1396 compacted_at: Some(1234),
1397 },
1398 MessagePart::Text {
1399 text: " suffix".into(),
1400 },
1401 ],
1402 );
1403 assert!(msg.content.contains("(pruned)"));
1404 assert!(msg.content.contains("prefix "));
1405 assert!(msg.content.contains(" suffix"));
1406 }
1407
1408 #[test]
1409 fn flatten_compacted_tool_output_with_reference_renders_body() {
1410 let ref_notice = "[tool output pruned; full content at /tmp/overflow/big.txt]";
1412 let msg = Message::from_parts(
1413 Role::User,
1414 vec![MessagePart::ToolOutput {
1415 tool_name: "bash".into(),
1416 body: ref_notice.into(),
1417 compacted_at: Some(1234),
1418 }],
1419 );
1420 assert!(msg.content.contains(ref_notice));
1421 assert!(!msg.content.contains("(pruned)"));
1422 }
1423
1424 #[test]
1425 fn rebuild_content_syncs_after_mutation() {
1426 let mut msg = Message::from_parts(
1427 Role::User,
1428 vec![MessagePart::ToolOutput {
1429 tool_name: "bash".into(),
1430 body: "original".into(),
1431 compacted_at: None,
1432 }],
1433 );
1434 assert!(msg.content.contains("original"));
1435
1436 if let MessagePart::ToolOutput {
1437 ref mut compacted_at,
1438 ref mut body,
1439 ..
1440 } = msg.parts[0]
1441 {
1442 *compacted_at = Some(999);
1443 body.clear(); }
1445 msg.rebuild_content();
1446
1447 assert!(msg.content.contains("(pruned)"));
1448 assert!(!msg.content.contains("original"));
1449 }
1450
1451 #[test]
1452 fn message_part_tool_use_serde_round_trip() {
1453 let part = MessagePart::ToolUse {
1454 id: "toolu_123".into(),
1455 name: "bash".into(),
1456 input: serde_json::json!({"command": "ls"}),
1457 };
1458 let json = serde_json::to_string(&part).unwrap();
1459 let deserialized: MessagePart = serde_json::from_str(&json).unwrap();
1460 if let MessagePart::ToolUse { id, name, input } = deserialized {
1461 assert_eq!(id, "toolu_123");
1462 assert_eq!(name, "bash");
1463 assert_eq!(input["command"], "ls");
1464 } else {
1465 panic!("expected ToolUse");
1466 }
1467 }
1468
1469 #[test]
1470 fn message_part_tool_result_serde_round_trip() {
1471 let part = MessagePart::ToolResult {
1472 tool_use_id: "toolu_123".into(),
1473 content: "file1.rs\nfile2.rs".into(),
1474 is_error: false,
1475 };
1476 let json = serde_json::to_string(&part).unwrap();
1477 let deserialized: MessagePart = serde_json::from_str(&json).unwrap();
1478 if let MessagePart::ToolResult {
1479 tool_use_id,
1480 content,
1481 is_error,
1482 } = deserialized
1483 {
1484 assert_eq!(tool_use_id, "toolu_123");
1485 assert_eq!(content, "file1.rs\nfile2.rs");
1486 assert!(!is_error);
1487 } else {
1488 panic!("expected ToolResult");
1489 }
1490 }
1491
1492 #[test]
1493 fn message_part_tool_result_is_error_default() {
1494 let json = r#"{"kind":"tool_result","tool_use_id":"id","content":"err"}"#;
1495 let part: MessagePart = serde_json::from_str(json).unwrap();
1496 if let MessagePart::ToolResult { is_error, .. } = part {
1497 assert!(!is_error);
1498 } else {
1499 panic!("expected ToolResult");
1500 }
1501 }
1502
1503 #[test]
1504 fn chat_response_construction() {
1505 let text = ChatResponse::Text("hello".into());
1506 assert_matches!(text, ChatResponse::Text(s) if s == "hello");
1507
1508 let tool_use = ChatResponse::ToolUse {
1509 text: Some("I'll run that".into()),
1510 tool_calls: vec![ToolUseRequest {
1511 id: "1".into(),
1512 name: "bash".into(),
1513 input: serde_json::json!({}),
1514 }],
1515 thinking_blocks: vec![],
1516 };
1517 assert_matches!(tool_use, ChatResponse::ToolUse { .. });
1518 }
1519
1520 #[test]
1521 fn flatten_parts_tool_use() {
1522 let msg = Message::from_parts(
1523 Role::Assistant,
1524 vec![MessagePart::ToolUse {
1525 id: "t1".into(),
1526 name: "bash".into(),
1527 input: serde_json::json!({"command": "ls"}),
1528 }],
1529 );
1530 assert!(msg.content.contains("[tool_use: bash(t1)]"));
1531 }
1532
1533 #[test]
1534 fn flatten_parts_tool_result() {
1535 let msg = Message::from_parts(
1536 Role::User,
1537 vec![MessagePart::ToolResult {
1538 tool_use_id: "t1".into(),
1539 content: "output here".into(),
1540 is_error: false,
1541 }],
1542 );
1543 assert!(msg.content.contains("[tool_result: t1]"));
1544 assert!(msg.content.contains("output here"));
1545 }
1546
1547 #[test]
1548 fn tool_definition_serde_round_trip() {
1549 let def = ToolDefinition {
1550 name: "bash".into(),
1551 description: "Execute a shell command".into(),
1552 parameters: serde_json::json!({"type": "object"}),
1553 output_schema: None,
1554 };
1555 let json = serde_json::to_string(&def).unwrap();
1556 let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap();
1557 assert_eq!(deserialized.name, "bash");
1558 assert_eq!(deserialized.description, "Execute a shell command");
1559 }
1560
1561 #[tokio::test]
1562 async fn chat_with_tools_default_delegates_to_chat() {
1563 let provider = StubProvider {
1564 response: "hello".into(),
1565 };
1566 let messages = vec![Message::from_legacy(Role::User, "test")];
1567 let result = provider.chat_with_tools(&messages, &[]).await.unwrap();
1568 assert_matches!(result, ChatResponse::Text(s) if s == "hello");
1569 }
1570
1571 #[test]
1572 fn tool_output_compacted_at_serde_default() {
1573 let json = r#"{"kind":"tool_output","tool_name":"bash","body":"out"}"#;
1574 let part: MessagePart = serde_json::from_str(json).unwrap();
1575 if let MessagePart::ToolOutput { compacted_at, .. } = part {
1576 assert!(compacted_at.is_none());
1577 } else {
1578 panic!("expected ToolOutput");
1579 }
1580 }
1581
1582 #[test]
1585 fn strip_json_fences_plain_json() {
1586 assert_eq!(strip_json_fences(r#"{"a": 1}"#), r#"{"a": 1}"#);
1587 }
1588
1589 #[test]
1590 fn strip_json_fences_with_json_fence() {
1591 assert_eq!(strip_json_fences("```json\n{\"a\": 1}\n```"), r#"{"a": 1}"#);
1592 }
1593
1594 #[test]
1595 fn strip_json_fences_with_plain_fence() {
1596 assert_eq!(strip_json_fences("```\n{\"a\": 1}\n```"), r#"{"a": 1}"#);
1597 }
1598
1599 #[test]
1600 fn strip_json_fences_whitespace() {
1601 assert_eq!(strip_json_fences(" \n "), "");
1602 }
1603
1604 #[test]
1605 fn strip_json_fences_empty() {
1606 assert_eq!(strip_json_fences(""), "");
1607 }
1608
1609 #[test]
1610 fn strip_json_fences_outer_whitespace() {
1611 assert_eq!(
1612 strip_json_fences(" ```json\n{\"a\": 1}\n``` "),
1613 r#"{"a": 1}"#
1614 );
1615 }
1616
1617 #[test]
1618 fn strip_json_fences_only_opening_fence() {
1619 assert_eq!(strip_json_fences("```json\n{\"a\": 1}"), r#"{"a": 1}"#);
1620 }
1621
1622 #[derive(Debug, serde::Deserialize, schemars::JsonSchema, PartialEq)]
1625 struct TestOutput {
1626 value: String,
1627 }
1628
1629 struct SequentialStub {
1630 responses: std::sync::Mutex<Vec<Result<String, LlmError>>>,
1631 }
1632
1633 impl SequentialStub {
1634 fn new(responses: Vec<Result<String, LlmError>>) -> Self {
1635 Self {
1636 responses: std::sync::Mutex::new(responses),
1637 }
1638 }
1639 }
1640
1641 impl LlmProvider for SequentialStub {
1642 async fn chat(&self, _messages: &[Message]) -> Result<String, LlmError> {
1643 let mut responses = self.responses.lock().unwrap();
1644 if responses.is_empty() {
1645 return Err(LlmError::Other("no more responses".into()));
1646 }
1647 responses.remove(0)
1648 }
1649
1650 async fn chat_stream(&self, messages: &[Message]) -> Result<ChatStream, LlmError> {
1651 let response = self.chat(messages).await?;
1652 Ok(Box::pin(tokio_stream::once(Ok(StreamChunk::Content(
1653 response,
1654 )))))
1655 }
1656
1657 fn supports_streaming(&self) -> bool {
1658 false
1659 }
1660
1661 async fn embed(&self, _text: &str) -> Result<Vec<f32>, LlmError> {
1662 Err(LlmError::EmbedUnsupported {
1663 provider: "sequential-stub".into(),
1664 })
1665 }
1666
1667 fn supports_embeddings(&self) -> bool {
1668 false
1669 }
1670
1671 fn name(&self) -> &'static str {
1672 "sequential-stub"
1673 }
1674 }
1675
1676 #[tokio::test]
1677 async fn chat_typed_happy_path() {
1678 let provider = StubProvider {
1679 response: r#"{"value": "hello"}"#.into(),
1680 };
1681 let messages = vec![Message::from_legacy(Role::User, "test")];
1682 let result: TestOutput = provider.chat_typed(&messages).await.unwrap();
1683 assert_eq!(
1684 result,
1685 TestOutput {
1686 value: "hello".into()
1687 }
1688 );
1689 }
1690
1691 #[tokio::test]
1692 async fn chat_typed_retry_succeeds() {
1693 let provider = SequentialStub::new(vec![
1694 Ok("not valid json".into()),
1695 Ok(r#"{"value": "ok"}"#.into()),
1696 ]);
1697 let messages = vec![Message::from_legacy(Role::User, "test")];
1698 let result: TestOutput = provider.chat_typed(&messages).await.unwrap();
1699 assert_eq!(result, TestOutput { value: "ok".into() });
1700 }
1701
1702 #[tokio::test]
1703 async fn chat_typed_both_fail() {
1704 let provider = SequentialStub::new(vec![Ok("bad json".into()), Ok("still bad".into())]);
1705 let messages = vec![Message::from_legacy(Role::User, "test")];
1706 let result = provider.chat_typed::<TestOutput>(&messages).await;
1707 let err = result.unwrap_err();
1708 assert!(err.to_string().contains("parse failed after retry"));
1709 }
1710
1711 #[tokio::test]
1712 async fn chat_typed_chat_error_propagates() {
1713 let provider = SequentialStub::new(vec![Err(LlmError::Unavailable)]);
1714 let messages = vec![Message::from_legacy(Role::User, "test")];
1715 let result = provider.chat_typed::<TestOutput>(&messages).await;
1716 assert_matches!(result, Err(LlmError::Unavailable));
1717 }
1718
1719 #[tokio::test]
1720 async fn chat_typed_strips_fences() {
1721 let provider = StubProvider {
1722 response: "```json\n{\"value\": \"fenced\"}\n```".into(),
1723 };
1724 let messages = vec![Message::from_legacy(Role::User, "test")];
1725 let result: TestOutput = provider.chat_typed(&messages).await.unwrap();
1726 assert_eq!(
1727 result,
1728 TestOutput {
1729 value: "fenced".into()
1730 }
1731 );
1732 }
1733
1734 #[test]
1735 fn supports_structured_output_default_false() {
1736 let provider = StubProvider {
1737 response: String::new(),
1738 };
1739 assert!(!provider.supports_structured_output());
1740 }
1741
1742 #[test]
1743 fn structured_parse_error_display() {
1744 let err = LlmError::StructuredParse("test error".into());
1745 assert_eq!(
1746 err.to_string(),
1747 "structured output parse failed: test error"
1748 );
1749 }
1750
1751 #[test]
1752 fn message_part_image_roundtrip_json() {
1753 let part = MessagePart::Image(Box::new(ImageData {
1754 data: vec![1, 2, 3, 4],
1755 mime_type: "image/jpeg".into(),
1756 }));
1757 let json = serde_json::to_string(&part).unwrap();
1758 let decoded: MessagePart = serde_json::from_str(&json).unwrap();
1759 match decoded {
1760 MessagePart::Image(img) => {
1761 assert_eq!(img.data, vec![1, 2, 3, 4]);
1762 assert_eq!(img.mime_type, "image/jpeg");
1763 }
1764 _ => panic!("expected Image variant"),
1765 }
1766 }
1767
1768 #[test]
1769 fn flatten_parts_includes_image_placeholder() {
1770 let msg = Message::from_parts(
1771 Role::User,
1772 vec![
1773 MessagePart::Text {
1774 text: "see this".into(),
1775 },
1776 MessagePart::Image(Box::new(ImageData {
1777 data: vec![0u8; 100],
1778 mime_type: "image/png".into(),
1779 })),
1780 ],
1781 );
1782 let content = msg.to_llm_content();
1783 assert!(content.contains("see this"));
1784 assert!(content.contains("[image: image/png"));
1785 }
1786
1787 #[test]
1788 fn supports_vision_default_false() {
1789 let provider = StubProvider {
1790 response: String::new(),
1791 };
1792 assert!(!provider.supports_vision());
1793 }
1794
1795 #[test]
1796 fn message_metadata_default_both_visible() {
1797 let m = MessageMetadata::default();
1798 assert!(m.visibility.is_agent_visible());
1799 assert!(m.visibility.is_user_visible());
1800 assert_eq!(m.visibility, MessageVisibility::Both);
1801 assert!(m.compacted_at.is_none());
1802 }
1803
1804 #[test]
1805 fn message_metadata_agent_only() {
1806 let m = MessageMetadata::agent_only();
1807 assert!(m.visibility.is_agent_visible());
1808 assert!(!m.visibility.is_user_visible());
1809 assert_eq!(m.visibility, MessageVisibility::AgentOnly);
1810 }
1811
1812 #[test]
1813 fn message_metadata_user_only() {
1814 let m = MessageMetadata::user_only();
1815 assert!(!m.visibility.is_agent_visible());
1816 assert!(m.visibility.is_user_visible());
1817 assert_eq!(m.visibility, MessageVisibility::UserOnly);
1818 }
1819
1820 #[test]
1821 fn message_metadata_serde_default() {
1822 let json = r#"{"role":"user","content":"hello"}"#;
1823 let msg: Message = serde_json::from_str(json).unwrap();
1824 assert!(msg.metadata.visibility.is_agent_visible());
1825 assert!(msg.metadata.visibility.is_user_visible());
1826 }
1827
1828 #[test]
1829 fn message_metadata_round_trip() {
1830 let msg = Message {
1831 role: Role::User,
1832 content: "test".into(),
1833 parts: vec![],
1834 metadata: MessageMetadata::agent_only(),
1835 };
1836 let json = serde_json::to_string(&msg).unwrap();
1837 let decoded: Message = serde_json::from_str(&json).unwrap();
1838 assert!(decoded.metadata.visibility.is_agent_visible());
1839 assert!(!decoded.metadata.visibility.is_user_visible());
1840 assert_eq!(decoded.metadata.visibility, MessageVisibility::AgentOnly);
1841 }
1842
1843 #[test]
1844 fn message_part_compaction_round_trip() {
1845 let part = MessagePart::Compaction {
1846 summary: "Context was summarized.".to_owned(),
1847 };
1848 let json = serde_json::to_string(&part).unwrap();
1849 let decoded: MessagePart = serde_json::from_str(&json).unwrap();
1850 assert!(
1851 matches!(decoded, MessagePart::Compaction { summary } if summary == "Context was summarized.")
1852 );
1853 }
1854
1855 #[test]
1856 fn flatten_parts_compaction_contributes_no_text() {
1857 let parts = vec![
1860 MessagePart::Text {
1861 text: "Hello".to_owned(),
1862 },
1863 MessagePart::Compaction {
1864 summary: "Summary".to_owned(),
1865 },
1866 ];
1867 let msg = Message::from_parts(Role::Assistant, parts);
1868 assert_eq!(msg.content.trim(), "Hello");
1870 }
1871
1872 #[test]
1873 fn stream_chunk_compaction_variant() {
1874 let chunk = StreamChunk::Compaction("A summary".to_owned());
1875 assert_matches!(chunk, StreamChunk::Compaction(s) if s == "A summary");
1876 }
1877
1878 #[test]
1879 fn short_type_name_extracts_last_segment() {
1880 struct MyOutput;
1881 assert_eq!(short_type_name::<MyOutput>(), "MyOutput");
1882 }
1883
1884 #[test]
1885 fn short_type_name_primitive_returns_full_name() {
1886 assert_eq!(short_type_name::<u32>(), "u32");
1888 assert_eq!(short_type_name::<bool>(), "bool");
1889 }
1890
1891 #[test]
1892 fn short_type_name_nested_path_returns_last() {
1893 assert_eq!(
1895 short_type_name::<std::collections::HashMap<u32, u32>>(),
1896 "HashMap<u32, u32>"
1897 );
1898 }
1899
1900 #[test]
1903 fn summary_roundtrip() {
1904 let part = MessagePart::Summary {
1905 text: "hello".to_string(),
1906 };
1907 let json = serde_json::to_string(&part).expect("serialization must not fail");
1908 assert!(
1909 json.contains("\"kind\":\"summary\""),
1910 "must use internally-tagged format, got: {json}"
1911 );
1912 assert!(
1913 !json.contains("\"Summary\""),
1914 "must not use externally-tagged format, got: {json}"
1915 );
1916 let decoded: MessagePart =
1917 serde_json::from_str(&json).expect("deserialization must not fail");
1918 match decoded {
1919 MessagePart::Summary { text } => assert_eq!(text, "hello"),
1920 other => panic!("expected MessagePart::Summary, got {other:?}"),
1921 }
1922 }
1923
1924 #[tokio::test]
1925 async fn embed_batch_default_empty_returns_empty() {
1926 let provider = StubProvider {
1927 response: String::new(),
1928 };
1929 let result = provider.embed_batch(&[]).await.unwrap();
1930 assert!(result.is_empty());
1931 }
1932
1933 #[tokio::test]
1934 async fn embed_batch_default_calls_embed_sequentially() {
1935 let provider = StubProvider {
1936 response: String::new(),
1937 };
1938 let texts = ["hello", "world", "foo"];
1939 let result = provider.embed_batch(&texts).await.unwrap();
1940 assert_eq!(result.len(), 3);
1941 for vec in &result {
1943 assert_eq!(vec, &[0.1_f32, 0.2, 0.3]);
1944 }
1945 }
1946
1947 #[test]
1948 fn message_visibility_db_roundtrip_both() {
1949 assert_eq!(MessageVisibility::Both.as_db_str(), "both");
1950 assert_eq!(
1951 MessageVisibility::from_db_str("both"),
1952 MessageVisibility::Both
1953 );
1954 }
1955
1956 #[test]
1957 fn message_visibility_db_roundtrip_agent_only() {
1958 assert_eq!(MessageVisibility::AgentOnly.as_db_str(), "agent_only");
1959 assert_eq!(
1960 MessageVisibility::from_db_str("agent_only"),
1961 MessageVisibility::AgentOnly
1962 );
1963 }
1964
1965 #[test]
1966 fn message_visibility_db_roundtrip_user_only() {
1967 assert_eq!(MessageVisibility::UserOnly.as_db_str(), "user_only");
1968 assert_eq!(
1969 MessageVisibility::from_db_str("user_only"),
1970 MessageVisibility::UserOnly
1971 );
1972 }
1973
1974 #[test]
1975 fn message_visibility_from_db_str_unknown_defaults_to_both() {
1976 assert_eq!(
1977 MessageVisibility::from_db_str("unknown_future_value"),
1978 MessageVisibility::Both
1979 );
1980 assert_eq!(MessageVisibility::from_db_str(""), MessageVisibility::Both);
1981 }
1982}