cetk/
context_manager.rs

1//! Context management and agent state persistence.
2//!
3//! This module provides the core context management functionality for CETK, including:
4//!
5//! - **Agent State Management**: Loading and persisting complete agent histories
6//! - **Transaction Building**: Fluent API for creating and persisting transactions
7//! - **Virtual Filesystems**: File operations within agent contexts
8//! - **Chroma Integration**: Vector storage and retrieval of agent data
9//!
10//! ## Core Types
11//!
12//! - [`ContextManager`]: Central coordinator for agent persistence operations
13//! - [`AgentData`]: Complete agent state with all contexts and transactions
14//! - [`AgentContext`]: Logical grouping of related transactions
15//! - [`TransactionBuilder`]: Fluent interface for transaction construction
16//!
17//! ## Usage Patterns
18//!
19//! ### Basic Agent Operations
20//!
21//! ```rust,no_run
22//! use cetk::{ContextManager, AgentID};
23//! use chroma::ChromaHttpClient;
24//!
25//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
26//! // Setup
27//! let client = ChromaHttpClient::cloud()?;
28//! let collection = client.get_or_create_collection("agents", None, None).await?;
29//! let context_manager = ContextManager::new(collection)?;
30//!
31//! let agent_id = AgentID::generate().unwrap();
32//!
33//! // Load existing agent or create new
34//! let mut agent_data = context_manager.load_agent(agent_id).await?;
35//!
36//! // Add a new transaction
37//! let nonce = agent_data
38//!     .next_transaction(&context_manager)
39//!     .message(claudius::MessageParam::user("Hello!"))
40//!     .save()
41//!     .await?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! ### File System Operations
47//!
48//! ```rust,no_run
49//! use cetk::{ContextManager, AgentID, MountID};
50//!
51//! # async fn example(context_manager: ContextManager, mut agent_data: cetk::AgentData) -> anyhow::Result<()> {
52//! let mount_id = MountID::generate().unwrap();
53//!
54//! // Write files and search content
55//! let _nonce = agent_data
56//!     .next_transaction(&context_manager)
57//!     .write_file(mount_id, "/notes.txt", "Important notes here")
58//!     .unwrap()
59//!     .write_file(mount_id, "/config.json", r#"{"theme": "dark"}"#)
60//!     .unwrap()
61//!     .save()
62//!     .await?;
63//!
64//! // Search file contents
65//! let matches = agent_data.search_file_contents(mount_id, "Important")?;
66//! println!("Found {} matching files", matches.len());
67//! # Ok(())
68//! # }
69//! ```
70//!
71//! ## Error Handling
72//!
73//! All operations that can fail return [`ContextManagerError`] which provides detailed
74//! error information including the specific operation that failed and underlying causes.
75//!
76//! ## Concurrency and Atomicity
77//!
78//! The [`ContextManager`] ensures atomic persistence of transactions through:
79//!
80//! - Chunk-based storage for large transactions
81//! - Nonce-based verification of successful persistence
82//! - Automatic transaction reassembly from chunks
83//!
84//! Individual transactions are atomic, but concurrent access to the same agent
85//! should be coordinated by the application layer.
86
87use std::collections::HashMap;
88
89use chroma::{
90    types::{
91        GetResponse, Include, IncludeList, Metadata, MetadataComparison, MetadataExpression,
92        MetadataValue, PrimitiveOperator, Where,
93    },
94    ChromaCollection,
95};
96
97use crate::{AgentID, EmbeddingService, FileWrite, Transaction, TransactionChunk, TransactionID};
98use claudius::MessageParam;
99
100////////////////////////////////////////////// Errors //////////////////////////////////////////////
101
102/// Error that can occur during context management operations.
103#[derive(Debug)]
104pub enum ContextManagerError {
105    /// Error connecting to or communicating with Chroma.
106    ChromaError(String),
107    /// Error chunking the transaction.
108    ChunkingError(crate::TransactionSerializationError),
109    /// GUID generation failed.
110    GuidError,
111    /// Error generating embeddings.
112    EmbeddingError(anyhow::Error),
113    /// Error loading agent data.
114    LoadAgentError(String),
115    /// Error with file operations.
116    FileError(FileSystemError),
117}
118
119/// Specific errors for file system operations
120#[derive(Debug)]
121pub enum FileSystemError {
122    /// Invalid file path
123    InvalidPath(String),
124    /// File not found
125    FileNotFound(String),
126    /// Empty or invalid search pattern
127    InvalidPattern(String),
128    /// Content not found for replacement
129    ContentNotFound(String),
130}
131
132impl std::fmt::Display for FileSystemError {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            FileSystemError::InvalidPath(msg) => write!(f, "Invalid file path: {}", msg),
136            FileSystemError::FileNotFound(path) => write!(f, "File not found: {}", path),
137            FileSystemError::InvalidPattern(msg) => write!(f, "Invalid search pattern: {}", msg),
138            FileSystemError::ContentNotFound(msg) => write!(f, "Content not found: {}", msg),
139        }
140    }
141}
142
143impl std::error::Error for FileSystemError {}
144
145impl From<FileSystemError> for ContextManagerError {
146    fn from(error: FileSystemError) -> Self {
147        ContextManagerError::FileError(error)
148    }
149}
150
151impl std::fmt::Display for ContextManagerError {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            ContextManagerError::ChromaError(e) => write!(f, "Chroma error: {}", e),
155            ContextManagerError::ChunkingError(e) => {
156                write!(f, "Transaction chunking error: {}", e)
157            }
158            ContextManagerError::GuidError => write!(f, "Failed to generate GUID"),
159            ContextManagerError::EmbeddingError(e) => write!(f, "Embedding error: {}", e),
160            ContextManagerError::LoadAgentError(e) => write!(f, "Agent loading error: {}", e),
161            ContextManagerError::FileError(e) => write!(f, "File system error: {}", e),
162        }
163    }
164}
165
166impl std::error::Error for ContextManagerError {
167    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168        match self {
169            ContextManagerError::ChromaError(_) => None,
170            ContextManagerError::ChunkingError(e) => Some(e),
171            ContextManagerError::GuidError => None,
172            ContextManagerError::EmbeddingError(e) => Some(e.as_ref()),
173            ContextManagerError::LoadAgentError(_) => None,
174            ContextManagerError::FileError(e) => Some(e),
175        }
176    }
177}
178
179impl From<crate::TransactionSerializationError> for ContextManagerError {
180    fn from(error: crate::TransactionSerializationError) -> Self {
181        ContextManagerError::ChunkingError(error)
182    }
183}
184
185impl From<anyhow::Error> for ContextManagerError {
186    fn from(error: anyhow::Error) -> Self {
187        ContextManagerError::EmbeddingError(error)
188    }
189}
190
191//////////////////////////////////////// Helper Functions /////////////////////////////////////////
192
193/// Extract a string field from Chroma metadata with error handling.
194///
195/// This helper function safely extracts string values from Chroma metadata,
196/// providing detailed error messages when fields are missing or have incorrect types.
197fn extract_metadata_string(
198    metadata: &HashMap<String, MetadataValue>,
199    field_name: &str,
200    chunk_id: &str,
201) -> Result<String, ContextManagerError> {
202    metadata
203        .get(field_name)
204        .and_then(|v| match v {
205            MetadataValue::Str(s) => Some(s.clone()),
206            _ => None,
207        })
208        .ok_or_else(|| {
209            ContextManagerError::LoadAgentError(format!(
210                "Missing or invalid {} in chunk {}",
211                field_name, chunk_id
212            ))
213        })
214}
215
216/// Extract a u32 field from Chroma metadata with error handling.
217///
218/// This helper function safely extracts u32 values from Chroma metadata,
219/// converting from i64 storage format and providing detailed error messages.
220fn extract_metadata_u32(
221    metadata: &HashMap<String, MetadataValue>,
222    field_name: &str,
223    chunk_id: &str,
224) -> Result<u32, ContextManagerError> {
225    metadata
226        .get(field_name)
227        .and_then(|v| match v {
228            MetadataValue::Int(i) => Some(*i as u32),
229            _ => None,
230        })
231        .ok_or_else(|| {
232            ContextManagerError::LoadAgentError(format!(
233                "Missing or invalid {} in chunk {}",
234                field_name, chunk_id
235            ))
236        })
237}
238
239/// Extract a u64 field from Chroma metadata with error handling.
240///
241/// This helper function safely extracts u64 values from Chroma metadata,
242/// converting from i64 storage format and providing detailed error messages.
243fn extract_metadata_u64(
244    metadata: &HashMap<String, MetadataValue>,
245    field_name: &str,
246    chunk_id: &str,
247) -> Result<u64, ContextManagerError> {
248    metadata
249        .get(field_name)
250        .and_then(|v| match v {
251            MetadataValue::Int(i) => Some(*i as u64),
252            _ => None,
253        })
254        .ok_or_else(|| {
255            ContextManagerError::LoadAgentError(format!(
256                "Missing or invalid {} in chunk {}",
257                field_name, chunk_id
258            ))
259        })
260}
261
262/// Validates a file path for security and correctness.
263///
264/// This function performs comprehensive validation to prevent:
265/// - Path traversal attacks ("../" sequences)
266/// - Null byte injection
267/// - Overly long paths (DoS prevention)
268/// - Invalid path formats
269///
270/// # Security
271///
272/// All file paths must start with "/" and cannot contain ".." to prevent
273/// access to files outside the virtual filesystem boundaries.
274fn validate_file_path(path: &str) -> Result<(), FileSystemError> {
275    if path.is_empty() {
276        return Err(FileSystemError::InvalidPath(
277            "File path cannot be empty".to_string(),
278        ));
279    }
280
281    if path.contains('\0') {
282        return Err(FileSystemError::InvalidPath(
283            "File path cannot contain null bytes".to_string(),
284        ));
285    }
286
287    // Check for path traversal attempts
288    if path.contains("..") {
289        return Err(FileSystemError::InvalidPath(
290            "File path cannot contain '..' (path traversal)".to_string(),
291        ));
292    }
293
294    // Ensure path starts with /
295    if !path.starts_with('/') {
296        return Err(FileSystemError::InvalidPath(
297            "File path must start with '/'".to_string(),
298        ));
299    }
300
301    // Check for reasonable length (prevent DoS)
302    if path.len() > 4096 {
303        return Err(FileSystemError::InvalidPath(
304            "File path too long (max 4096 characters)".to_string(),
305        ));
306    }
307
308    Ok(())
309}
310
311/// Generate a consistent chunk ID from transaction metadata.
312///
313/// Creates a unique identifier for each transaction chunk using a hierarchical
314/// format that enables efficient querying and sorting.
315///
316/// # Format
317///
318/// `agent_id:context_seq_no:transaction_seq_no:chunk_seq_no`
319///
320/// This format ensures that chunks sort naturally by agent, context, transaction,
321/// and chunk sequence, enabling efficient range queries in Chroma.
322fn generate_chunk_id(
323    agent_id: AgentID,
324    context_seq_no: u32,
325    transaction_seq_no: u64,
326    chunk_seq_no: u32,
327) -> String {
328    format!(
329        "{}:{}:{}:{}",
330        agent_id, context_seq_no, transaction_seq_no, chunk_seq_no
331    )
332}
333
334fn metadata_equals(
335    key: impl Into<String>,
336    value: impl Into<MetadataValue>,
337) -> Where {
338    Where::Metadata(MetadataExpression {
339        key: key.into(),
340        comparison: MetadataComparison::Primitive(PrimitiveOperator::Equal, value.into()),
341    })
342}
343
344/// Search for the most recent file content in a collection of contexts.
345///
346/// Traverses agent contexts in reverse chronological order to find the most
347/// recent write to a specific file path on a given mount.
348///
349/// # Returns
350///
351/// The content of the most recent file write, or `None` if the file was never written.
352///
353/// # Algorithm
354///
355/// Searches contexts from newest to oldest, then transactions within each context
356/// from newest to oldest, ensuring the most recent write is always found first.
357fn find_most_recent_file_content(
358    contexts: &[AgentContext],
359    mount: crate::MountID,
360    path: &str,
361) -> Option<String> {
362    // Search through all contexts in reverse chronological order
363    // to find the most recent write to this file
364    for context in contexts.iter().rev() {
365        for transaction in context.transactions.iter().rev() {
366            for write in transaction.writes.iter().rev() {
367                if write.mount == mount && write.path == path {
368                    return Some(write.data.clone());
369                }
370            }
371        }
372    }
373    None
374}
375
376//////////////////////////////////////// Agent Data Structures ////////////////////////////////////////
377
378/// Represents a complete context with its transactions for an agent.
379///
380/// A context is a logical grouping of related transactions within an agent's
381/// conversation or interaction history. Contexts provide a way to organize
382/// transactions into coherent sessions or conversation threads.
383#[derive(Debug, Clone)]
384pub struct AgentContext {
385    /// The unique identifier of the agent that owns this context
386    pub agent_id: AgentID,
387    /// The sequence number that uniquely identifies this context
388    pub context_seq_no: u32,
389    /// All transactions belonging to this context, ordered by sequence number
390    pub transactions: Vec<Transaction>,
391}
392
393/// Complete agent data with all contexts and transactions.
394///
395/// This structure represents the full persistent state for an agent,
396/// containing all of its contexts and the transactions within each context.
397/// It provides methods for navigating, querying, and building upon the
398/// agent's interaction history.
399///
400/// # Examples
401///
402/// ```rust
403/// use cetk::{AgentData, AgentID};
404///
405/// let agent_id = AgentID::generate().unwrap();
406/// let agent_data = AgentData {
407///     agent_id,
408///     contexts: Vec::new(),
409/// };
410///
411/// // Get the latest context
412/// let latest = agent_data.latest_context();
413/// assert!(latest.is_none()); // No contexts yet
414/// ```
415#[derive(Debug, Clone)]
416pub struct AgentData {
417    /// The unique identifier of the agent
418    pub agent_id: AgentID,
419    /// All contexts for this agent, ordered by sequence number
420    pub contexts: Vec<AgentContext>,
421}
422
423impl AgentData {
424    /// Get all transactions across all contexts, in order
425    pub fn all_transactions(&self) -> Vec<&Transaction> {
426        let mut transactions = Vec::new();
427        for context in &self.contexts {
428            transactions.extend(context.transactions.iter());
429        }
430        transactions
431    }
432
433    /// Get the latest context (highest context_seq_no)
434    pub fn latest_context(&self) -> Option<&AgentContext> {
435        self.contexts.iter().max_by_key(|c| c.context_seq_no)
436    }
437
438    /// Get a specific context by sequence number
439    pub fn get_context(&self, context_seq_no: u32) -> Option<&AgentContext> {
440        self.contexts
441            .iter()
442            .find(|c| c.context_seq_no == context_seq_no)
443    }
444
445    /// Start building the next transaction in the current context.
446    ///
447    /// This creates a fluent TransactionBuilder that automatically determines
448    /// the correct sequence numbers for continuing in the latest context.
449    pub fn next_transaction<'a>(
450        &'a mut self,
451        context_manager: &'a ContextManager,
452    ) -> TransactionBuilder<'a> {
453        TransactionBuilder::new_in_current_context(self, context_manager)
454    }
455
456    /// Start building the first transaction in a new context.
457    ///
458    /// This creates a fluent TransactionBuilder for starting a fresh context,
459    /// typically used for major conversation restarts or compaction.
460    pub fn new_context<'a>(
461        &'a mut self,
462        context_manager: &'a ContextManager,
463    ) -> TransactionBuilder<'a> {
464        TransactionBuilder::new_in_next_context(self, context_manager)
465    }
466
467    /// Get the content of a file from the most recent version in transaction history
468    pub fn get_file_content(
469        &self,
470        mount: crate::MountID,
471        path: &str,
472    ) -> Result<Option<String>, FileSystemError> {
473        validate_file_path(path)?;
474        Ok(find_most_recent_file_content(&self.contexts, mount, path))
475    }
476
477    /// List all files that have been written in transaction history for a mount
478    pub fn list_files(&self, mount: crate::MountID) -> Vec<String> {
479        use std::collections::HashSet;
480
481        // Collect file paths using iterator chain to reduce intermediate allocations
482        let mut files: HashSet<String> = self
483            .contexts
484            .iter()
485            .flat_map(|context| &context.transactions)
486            .flat_map(|transaction| &transaction.writes)
487            .filter(|write| write.mount == mount)
488            .map(|write| write.path.clone())
489            .collect();
490
491        // Convert to sorted Vec - can't avoid this allocation as we need ownership
492        let mut result: Vec<String> = files.drain().collect();
493        result.sort();
494        result
495    }
496
497    /// Search for files containing a pattern in their content
498    pub fn search_file_contents(
499        &self,
500        mount: crate::MountID,
501        pattern: &str,
502    ) -> Result<Vec<(String, Vec<String>)>, FileSystemError> {
503        if pattern.is_empty() {
504            return Err(FileSystemError::InvalidPattern(
505                "Search pattern cannot be empty".to_string(),
506            ));
507        }
508
509        let files = self.list_files(mount);
510
511        // Use iterator with filter_map to avoid intermediate allocations
512        let matches = files
513            .into_iter()
514            .filter_map(|file_path| {
515                // Only process files that exist and have valid content
516                match self.get_file_content(mount, &file_path) {
517                    Ok(Some(content)) => {
518                        // Use iterator to avoid intermediate Vec allocation
519                        let matching_lines: Vec<String> = content
520                            .lines()
521                            .enumerate()
522                            .filter_map(|(line_num, line)| {
523                                if line.contains(pattern) {
524                                    Some(format!("{}:{}", line_num + 1, line))
525                                } else {
526                                    None
527                                }
528                            })
529                            .collect();
530
531                        if matching_lines.is_empty() {
532                            None
533                        } else {
534                            Some((file_path, matching_lines))
535                        }
536                    }
537                    Ok(None) | Err(_) => None, // File doesn't exist or invalid path, skip
538                }
539            })
540            .collect();
541
542        Ok(matches)
543    }
544}
545
546//////////////////////////////////////// TransactionBuilder ////////////////////////////////////////
547
548/// A fluent builder for creating the next transaction from existing agent data.
549///
550/// This builder automatically determines the correct sequence numbers and provides
551/// a fluent API for adding messages and file writes.
552pub struct TransactionBuilder<'a> {
553    agent_data: &'a mut AgentData,
554    context_manager: &'a ContextManager,
555    context_seq_no: u32,
556    transaction_seq_no: u64,
557    msgs: Vec<MessageParam>,
558    writes: Vec<FileWrite>,
559}
560
561impl<'a> TransactionBuilder<'a> {
562    /// Helper to create a Transaction struct from the current builder state
563    fn create_transaction(&self) -> Transaction {
564        Transaction {
565            agent_id: self.agent_data.agent_id,
566            context_seq_no: self.context_seq_no,
567            transaction_seq_no: self.transaction_seq_no,
568            msgs: self.msgs.clone(),
569            writes: self.writes.clone(),
570        }
571    }
572
573    /// Helper to create and add a FileWrite after validation
574    fn add_validated_write<P: Into<String>, D: Into<String>>(
575        &mut self,
576        mount: crate::MountID,
577        path: P,
578        data: D,
579    ) -> Result<(), FileSystemError> {
580        let path_str = path.into();
581        validate_file_path(&path_str)?;
582
583        self.writes.push(FileWrite {
584            mount,
585            path: path_str,
586            data: data.into(),
587        });
588        Ok(())
589    }
590
591    /// Create a new transaction builder for the next transaction in the current context
592    fn new_in_current_context(
593        agent_data: &'a mut AgentData,
594        context_manager: &'a ContextManager,
595    ) -> Self {
596        let (context_seq_no, transaction_seq_no) =
597            if let Some(latest_context) = agent_data.latest_context() {
598                let next_transaction_seq = latest_context
599                    .transactions
600                    .iter()
601                    .map(|t| t.transaction_seq_no)
602                    .max()
603                    .unwrap_or(0)
604                    + 1;
605                (latest_context.context_seq_no, next_transaction_seq)
606            } else {
607                // No contexts exist, start with context 1, transaction 1
608                (1, 1)
609            };
610
611        TransactionBuilder {
612            agent_data,
613            context_manager,
614            context_seq_no,
615            transaction_seq_no,
616            msgs: Vec::new(),
617            writes: Vec::new(),
618        }
619    }
620
621    /// Create a new transaction builder for the next transaction in a new context
622    fn new_in_next_context(
623        agent_data: &'a mut AgentData,
624        context_manager: &'a ContextManager,
625    ) -> Self {
626        let next_context_seq = agent_data
627            .contexts
628            .iter()
629            .map(|c| c.context_seq_no)
630            .max()
631            .unwrap_or(0)
632            + 1;
633
634        TransactionBuilder {
635            agent_data,
636            context_manager,
637            context_seq_no: next_context_seq,
638            transaction_seq_no: 1, // First transaction in new context
639            msgs: Vec::new(),
640            writes: Vec::new(),
641        }
642    }
643
644    /// Add a message to this transaction
645    pub fn message(mut self, message: MessageParam) -> Self {
646        self.msgs.push(message);
647        self
648    }
649
650    /// Add multiple messages to this transaction
651    pub fn messages(mut self, messages: Vec<MessageParam>) -> Self {
652        self.msgs.extend(messages);
653        self
654    }
655
656    /// Add a file write to this transaction
657    pub fn write_file<P: Into<String>, D: Into<String>>(
658        mut self,
659        mount: crate::MountID,
660        path: P,
661        data: D,
662    ) -> Result<Self, FileSystemError> {
663        self.add_validated_write(mount, path, data)?;
664        Ok(self)
665    }
666
667    /// Add multiple file writes to this transaction
668    pub fn write_files(mut self, writes: Vec<FileWrite>) -> Result<Self, FileSystemError> {
669        // Validate all paths before adding any writes
670        for write in &writes {
671            validate_file_path(&write.path)?;
672        }
673
674        self.writes.extend(writes);
675        Ok(self)
676    }
677
678    /// Perform a string replacement in an existing file and add the result as a write
679    pub fn str_replace_file<P: Into<String>>(
680        mut self,
681        mount: crate::MountID,
682        path: P,
683        old_content: &str,
684        new_content: &str,
685    ) -> Result<Self, FileSystemError> {
686        let path_str = path.into();
687        match self.agent_data.get_file_content(mount, &path_str)? {
688            Some(current_content) => {
689                if !current_content.contains(old_content) {
690                    return Err(FileSystemError::ContentNotFound(format!(
691                        "Content '{}' not found in file {}",
692                        old_content, path_str
693                    )));
694                }
695                let updated_content = current_content.replace(old_content, new_content);
696                self.writes.push(FileWrite {
697                    mount,
698                    path: path_str,
699                    data: updated_content,
700                });
701                Ok(self)
702            }
703            None => Err(FileSystemError::FileNotFound(path_str)),
704        }
705    }
706
707    /// Insert content into a file (creates new file or overwrites existing)
708    pub fn insert_file<P: Into<String>, D: Into<String>>(
709        mut self,
710        mount: crate::MountID,
711        path: P,
712        content: D,
713    ) -> Result<Self, FileSystemError> {
714        self.add_validated_write(mount, path, content)?;
715        Ok(self)
716    }
717
718    /// Get the current buffered content for a file (considering both transaction history and pending writes)
719    pub fn get_buffered_content(
720        &self,
721        mount: crate::MountID,
722        path: &str,
723    ) -> Result<Option<String>, FileSystemError> {
724        validate_file_path(path)?;
725
726        // First check pending writes in reverse order (most recent first)
727        for write in self.writes.iter().rev() {
728            if write.mount == mount && write.path == path {
729                return Ok(Some(write.data.clone()));
730            }
731        }
732        // Fall back to transaction history
733        self.agent_data.get_file_content(mount, path)
734    }
735
736    /// View file content (same as get_buffered_content but returns Option instead of Result<Option>)
737    pub fn view_file(&self, mount: crate::MountID, path: &str) -> Option<String> {
738        self.get_buffered_content(mount, path).ok().flatten()
739    }
740
741    /// List all files for a mount (combines transaction history and pending writes)
742    pub fn list_files(&self, mount: crate::MountID) -> Vec<String> {
743        use std::collections::HashSet;
744
745        // Start with files from transaction history
746        let mut files: HashSet<String> = self.agent_data.list_files(mount).into_iter().collect();
747
748        // Add files from pending writes
749        for write in &self.writes {
750            if write.mount == mount {
751                files.insert(write.path.clone());
752            }
753        }
754
755        let mut result: Vec<String> = files.into_iter().collect();
756        result.sort();
757        result
758    }
759
760    /// Search for files containing a pattern (combines transaction history and pending writes)
761    pub fn search_files(&self, mount: crate::MountID, pattern: &str) -> Vec<(String, Vec<String>)> {
762        let files = self.list_files(mount);
763
764        files
765            .into_iter()
766            .filter_map(|file_path| {
767                // Get content using get_buffered_content to check pending writes too
768                match self.get_buffered_content(mount, &file_path) {
769                    Ok(Some(content)) => {
770                        let matching_lines: Vec<String> = content
771                            .lines()
772                            .enumerate()
773                            .filter_map(|(line_num, line)| {
774                                if line.contains(pattern) {
775                                    Some(format!("{}:{}", line_num + 1, line))
776                                } else {
777                                    None
778                                }
779                            })
780                            .collect();
781
782                        if matching_lines.is_empty() {
783                            None
784                        } else {
785                            Some((file_path, matching_lines))
786                        }
787                    }
788                    Ok(None) | Err(_) => None,
789                }
790            })
791            .collect()
792    }
793
794    /// Get a summary of all pending writes
795    pub fn get_write_summary(&self) -> Vec<(crate::MountID, String, usize)> {
796        let mut summary = Vec::new();
797        for write in &self.writes {
798            summary.push((write.mount, write.path.clone(), write.data.len()));
799        }
800        summary
801    }
802
803    /// Complete the transaction builder and persist it to Chroma.
804    ///
805    /// This will:
806    /// 1. Build the Transaction struct
807    /// 2. Persist it to Chroma with verification
808    /// 3. Update the AgentData with the new transaction
809    /// 4. Return the persistence nonce
810    pub async fn save(mut self) -> Result<String, ContextManagerError> {
811        // Build the transaction
812        let transaction = self.create_transaction();
813
814        // Persist to Chroma
815        let nonce = self
816            .context_manager
817            .persist_transaction(&transaction)
818            .await?;
819
820        // Update the agent data
821        self.update_agent_data(transaction);
822
823        Ok(nonce)
824    }
825
826    /// Build the transaction without persisting it.
827    ///
828    /// This is useful for testing or when you want to inspect the transaction
829    /// before persisting it.
830    pub fn build(self) -> Transaction {
831        self.create_transaction()
832    }
833
834    /// Update the AgentData with the new transaction
835    fn update_agent_data(&mut self, transaction: Transaction) {
836        // Find or create the context
837        if let Some(context) = self
838            .agent_data
839            .contexts
840            .iter_mut()
841            .find(|c| c.context_seq_no == self.context_seq_no)
842        {
843            // Add to existing context
844            context.transactions.push(transaction);
845            // Keep transactions sorted
846            context.transactions.sort_by_key(|t| t.transaction_seq_no);
847        } else {
848            // Create new context
849            let new_context = AgentContext {
850                agent_id: self.agent_data.agent_id,
851                context_seq_no: self.context_seq_no,
852                transactions: vec![transaction],
853            };
854            self.agent_data.contexts.push(new_context);
855            // Keep contexts sorted
856            self.agent_data.contexts.sort_by_key(|c| c.context_seq_no);
857        }
858    }
859}
860
861//////////////////////////////////////// ContextManager ////////////////////////////////////////
862
863/// Manages agent contexts and transaction persistence using Chroma collections.
864///
865/// The ContextManager handles:
866/// 1. Agent context management and transaction persistence
867/// 2. Atomically adds all transaction chunks to a chroma collection with nonce metadata
868/// 3. Can verify transaction persistence by checking if chunk[0] has the expected nonce
869pub struct ContextManager {
870    collection: ChromaCollection,
871    embedding_service: EmbeddingService,
872}
873
874impl ContextManager {
875    /// Create a new ContextManager with a Chroma collection.
876    pub fn new(collection: ChromaCollection) -> Result<Self, ContextManagerError> {
877        let embedding_service = EmbeddingService::new()?;
878        Ok(ContextManager {
879            collection,
880            embedding_service,
881        })
882    }
883
884    /// Persist a transaction by chunking it and atomically storing all chunks with a nonce.
885    ///
886    /// Automatically verifies persistence before returning success.
887    /// Returns the GUID that was used as the nonce if persistence and verification succeed.
888    pub async fn persist_transaction(
889        &self,
890        transaction: &Transaction,
891    ) -> Result<String, ContextManagerError> {
892        // Generate a unique GUID for this persistence operation using TransactionID
893        let nonce = TransactionID::generate()
894            .ok_or(ContextManagerError::GuidError)?
895            .to_string();
896
897        // Chunk the transaction
898        let chunks = transaction.chunk_transaction()?;
899
900        // Create IDs for the chunks
901        let chunk_ids: Vec<String> = chunks
902            .iter()
903            .map(|chunk| {
904                generate_chunk_id(
905                    chunk.agent_id,
906                    chunk.context_seq_no,
907                    chunk.transaction_seq_no,
908                    chunk.chunk_seq_no,
909                )
910            })
911            .collect();
912
913        // Create metadata with the nonce for each chunk
914        let metadatas: Vec<Metadata> = chunks
915            .iter()
916            .map(|chunk| {
917                let mut metadata: Metadata = HashMap::new();
918                metadata.insert("nonce".to_string(), MetadataValue::Str(nonce.clone()));
919                metadata.insert(
920                    "agent_id".to_string(),
921                    MetadataValue::Str(chunk.agent_id.to_string()),
922                );
923                metadata.insert(
924                    "context_seq_no".to_string(),
925                    MetadataValue::Int(chunk.context_seq_no as i64),
926                );
927                metadata.insert(
928                    "transaction_seq_no".to_string(),
929                    MetadataValue::Int(chunk.transaction_seq_no as i64),
930                );
931                metadata.insert(
932                    "chunk_seq_no".to_string(),
933                    MetadataValue::Int(chunk.chunk_seq_no as i64),
934                );
935                metadata.insert(
936                    "total_chunks".to_string(),
937                    MetadataValue::Int(chunk.total_chunks as i64),
938                );
939                metadata
940            })
941            .collect();
942        let metadata_entries: Vec<Option<Metadata>> =
943            metadatas.into_iter().map(Some).collect();
944
945        // Create documents from chunk data
946        let document_texts: Vec<String> = chunks.iter().map(|chunk| chunk.data.clone()).collect();
947        let document_refs: Vec<&str> = document_texts.iter().map(|doc| doc.as_str()).collect();
948
949        // Generate real embeddings for each chunk using the embedding service
950        let embeddings = self.embedding_service.embed(&document_refs)?;
951        let documents: Vec<Option<String>> =
952            document_texts.into_iter().map(Some).collect();
953
954        // Atomically add all chunks to the collection
955        self.collection
956            .add(chunk_ids, embeddings, Some(documents), None, Some(metadata_entries))
957            .await
958            .map_err(|e| ContextManagerError::ChromaError(e.to_string()))?;
959
960        // Verify persistence before returning success
961        let verification_successful = self.verify_persistence(transaction, &nonce).await?;
962        if !verification_successful {
963            return Err(ContextManagerError::ChromaError(
964                "Transaction persistence verification failed".to_string(),
965            ));
966        }
967
968        Ok(nonce)
969    }
970
971    /// Verify that a transaction was successfully persisted by checking chunk[0] for the expected nonce.
972    ///
973    /// Returns `true` if chunk[0] exists and has the specified nonce, `false` otherwise.
974    pub async fn verify_persistence(
975        &self,
976        transaction: &Transaction,
977        expected_nonce: &str,
978    ) -> Result<bool, ContextManagerError> {
979        // Construct the ID for chunk 0
980        let chunk_0_id = generate_chunk_id(
981            transaction.agent_id,
982            transaction.context_seq_no,
983            transaction.transaction_seq_no,
984            0,
985        );
986
987        // Try to get chunk 0 from the collection
988        let include = IncludeList(vec![Include::Metadata]);
989
990        let result = self
991            .collection
992            .get(
993                Some(vec![chunk_0_id]),
994                None,
995                Some(1),
996                Some(0),
997                Some(include),
998            )
999            .await
1000            .map_err(|e| ContextManagerError::ChromaError(e.to_string()))?;
1001
1002        // Check if we got exactly one result
1003        if result.ids.len() != 1 {
1004            return Ok(false);
1005        }
1006
1007        // Check if we have metadata and verify nonce
1008        if let Some(metadatas) = result.metadatas
1009            && let Some(Some(metadata)) = metadatas.first()
1010            && let Some(MetadataValue::Str(nonce_str)) = metadata.get("nonce")
1011        {
1012            return Ok(nonce_str == expected_nonce);
1013        }
1014
1015        Ok(false)
1016    }
1017
1018    /// Load all transaction data for a given agent, organizing it by contexts and transactions.
1019    ///
1020    /// This method:
1021    /// 1. Queries Chroma for all chunks belonging to the agent in batches (to handle 300 result limit)
1022    /// 2. Groups chunks by context and transaction
1023    /// 3. Assembles complete transactions from their chunks
1024    /// 4. Returns organized agent data with contexts and transactions
1025    pub async fn load_agent(&self, agent_id: AgentID) -> Result<AgentData, ContextManagerError> {
1026        let mut all_chunks = Vec::new();
1027        let batch_size: u32 = 300; // Chroma server-side limit
1028        let mut offset: u32 = 0;
1029
1030        loop {
1031            // Query chunks for this agent in batches
1032            let agent_filter = metadata_equals("agent_id", agent_id.to_string());
1033            let include = IncludeList(vec![Include::Metadata, Include::Document]);
1034
1035            let result = self
1036                .collection
1037                .get(
1038                    None,
1039                    Some(agent_filter),
1040                    Some(batch_size),
1041                    Some(offset),
1042                    Some(include),
1043                )
1044                .await
1045                .map_err(|e| ContextManagerError::ChromaError(e.to_string()))?;
1046
1047            // Convert Chroma result to TransactionChunks
1048            let batch_chunks = self.convert_chroma_result_to_chunks(result)?;
1049
1050            let batch_size_returned = batch_chunks.len();
1051            all_chunks.extend(batch_chunks);
1052
1053            // If we got fewer results than requested, we've reached the end
1054            if batch_size_returned < batch_size as usize {
1055                break;
1056            }
1057
1058            offset = offset.saturating_add(batch_size);
1059        }
1060
1061        // Organize chunks by context and transaction, then assemble
1062        let agent_data = self.assemble_agent_data(agent_id, all_chunks)?;
1063
1064        Ok(agent_data)
1065    }
1066
1067    /// Convert Chroma GetResponse to TransactionChunks
1068    fn convert_chroma_result_to_chunks(
1069        &self,
1070        result: GetResponse,
1071    ) -> Result<Vec<TransactionChunk>, ContextManagerError> {
1072        let GetResponse {
1073            ids,
1074            metadatas,
1075            documents,
1076            ..
1077        } = result;
1078
1079        let Some(metadatas) = metadatas else {
1080            return Err(ContextManagerError::LoadAgentError(
1081                "No metadata returned from Chroma".to_string(),
1082            ));
1083        };
1084
1085        let Some(documents) = documents else {
1086            return Err(ContextManagerError::LoadAgentError(
1087                "No documents returned from Chroma".to_string(),
1088            ));
1089        };
1090
1091        let mut chunks = Vec::new();
1092        for (i, id) in ids.iter().enumerate() {
1093            let metadata = metadatas.get(i).and_then(|m| m.as_ref()).ok_or_else(|| {
1094                ContextManagerError::LoadAgentError(format!("Missing metadata for chunk {}", id))
1095            })?;
1096
1097            let document = documents.get(i).and_then(|d| d.as_ref()).ok_or_else(|| {
1098                ContextManagerError::LoadAgentError(format!("Missing document for chunk {}", id))
1099            })?;
1100
1101            // Extract metadata fields using helper functions
1102            let agent_id_str = extract_metadata_string(metadata, "agent_id", id)?;
1103
1104            let agent_id = AgentID::from_human_readable(&agent_id_str).ok_or_else(|| {
1105                ContextManagerError::LoadAgentError(format!(
1106                    "Invalid agent_id format in chunk {}: {}",
1107                    id, agent_id_str
1108                ))
1109            })?;
1110
1111            let context_seq_no = extract_metadata_u32(metadata, "context_seq_no", id)?;
1112            let transaction_seq_no = extract_metadata_u64(metadata, "transaction_seq_no", id)?;
1113            let chunk_seq_no = extract_metadata_u32(metadata, "chunk_seq_no", id)?;
1114            let total_chunks = extract_metadata_u32(metadata, "total_chunks", id)?;
1115
1116            chunks.push(TransactionChunk {
1117                agent_id,
1118                context_seq_no,
1119                transaction_seq_no,
1120                chunk_seq_no,
1121                total_chunks,
1122                data: document.clone(),
1123            });
1124        }
1125
1126        Ok(chunks)
1127    }
1128
1129    /// Organize chunks into agent data with contexts and transactions
1130    fn assemble_agent_data(
1131        &self,
1132        agent_id: AgentID,
1133        chunks: Vec<TransactionChunk>,
1134    ) -> Result<AgentData, ContextManagerError> {
1135        use std::collections::BTreeMap;
1136
1137        // Group chunks by (context_seq_no, transaction_seq_no)
1138        let mut context_transaction_chunks: BTreeMap<(u32, u64), Vec<TransactionChunk>> =
1139            BTreeMap::new();
1140
1141        for chunk in chunks {
1142            let key = (chunk.context_seq_no, chunk.transaction_seq_no);
1143            context_transaction_chunks
1144                .entry(key)
1145                .or_default()
1146                .push(chunk);
1147        }
1148
1149        // Group by context_seq_no to build contexts
1150        let mut contexts_map: BTreeMap<u32, Vec<Transaction>> = BTreeMap::new();
1151
1152        for ((context_seq_no, _transaction_seq_no), mut transaction_chunks) in
1153            context_transaction_chunks
1154        {
1155            // Sort chunks by chunk_seq_no to ensure correct order
1156            transaction_chunks.sort_by_key(|c| c.chunk_seq_no);
1157
1158            // Assemble transaction from chunks
1159            let transaction = Transaction::from_chunks(transaction_chunks).map_err(|e| {
1160                ContextManagerError::LoadAgentError(format!(
1161                    "Failed to assemble transaction: {}",
1162                    e
1163                ))
1164            })?;
1165
1166            contexts_map
1167                .entry(context_seq_no)
1168                .or_default()
1169                .push(transaction);
1170        }
1171
1172        // Build final AgentContext structs, sorting transactions within each context
1173        let mut contexts = Vec::new();
1174        for (context_seq_no, mut transactions) in contexts_map {
1175            // Sort transactions by transaction_seq_no
1176            transactions.sort_by_key(|t| t.transaction_seq_no);
1177
1178            contexts.push(AgentContext {
1179                agent_id,
1180                context_seq_no,
1181                transactions,
1182            });
1183        }
1184
1185        // Sort contexts by context_seq_no
1186        contexts.sort_by_key(|c| c.context_seq_no);
1187
1188        Ok(AgentData { agent_id, contexts })
1189    }
1190}
1191
1192/////////////////////////////////////////////// tests //////////////////////////////////////////////
1193
1194#[cfg(test)]
1195mod tests {
1196    use super::*;
1197    use crate::AgentID;
1198    use chroma::ChromaHttpClient;
1199    use claudius::{MessageParam, MessageRole};
1200
1201    async fn create_test_client() -> ChromaHttpClient {
1202        ChromaHttpClient::cloud().expect("Failed to construct Chroma client")
1203    }
1204
1205    fn create_test_transaction() -> Transaction {
1206        Transaction {
1207            agent_id: AgentID::generate().unwrap(),
1208            context_seq_no: 1,
1209            transaction_seq_no: 42,
1210            msgs: vec![MessageParam {
1211                role: MessageRole::User,
1212                content: "Test message".into(),
1213            }],
1214            writes: vec![],
1215        }
1216    }
1217
1218    async fn create_test_context_manager() -> ContextManager {
1219        let client = create_test_client().await;
1220        let collection = client
1221            .get_or_create_collection("test_transactions", None, None)
1222            .await
1223            .expect("Failed to create Chroma collection");
1224        ContextManager::new(collection).expect("Failed to create ContextManager")
1225    }
1226
1227    #[tokio::test]
1228    async fn context_manager_creation() {
1229        let _context_manager = create_test_context_manager().await;
1230        // Test passes if we can create the context manager without panicking
1231    }
1232
1233    #[tokio::test]
1234    async fn persist_and_verify_transaction() {
1235        let context_manager = create_test_context_manager().await;
1236        let transaction = create_test_transaction();
1237
1238        // Persist the transaction
1239        let nonce_result = context_manager.persist_transaction(&transaction).await;
1240        assert!(nonce_result.is_ok());
1241
1242        let nonce = nonce_result.unwrap();
1243        assert!(!nonce.is_empty());
1244
1245        // Verify the persistence
1246        let verification_result = context_manager
1247            .verify_persistence(&transaction, &nonce)
1248            .await;
1249        assert!(verification_result.is_ok());
1250        assert!(verification_result.unwrap());
1251
1252        // Verify with wrong nonce should return false
1253        let wrong_nonce = TransactionID::generate().unwrap().to_string();
1254        let wrong_verification = context_manager
1255            .verify_persistence(&transaction, &wrong_nonce)
1256            .await;
1257        assert!(wrong_verification.is_ok());
1258        assert!(!wrong_verification.unwrap());
1259    }
1260
1261    #[tokio::test]
1262    async fn persist_transaction_with_multiple_chunks() {
1263        let client = create_test_client().await;
1264        let collection = client
1265            .get_or_create_collection("test_transactions", None, None)
1266            .await
1267            .expect("Failed to create Chroma collection");
1268        let context_manager =
1269            ContextManager::new(collection).expect("Failed to create ContextManager");
1270
1271        // Create a large transaction that will be chunked
1272        let large_content = "x".repeat(crate::CHUNK_SIZE_LIMIT * 2);
1273        let mut transaction = create_test_transaction();
1274        transaction.msgs.push(MessageParam {
1275            role: MessageRole::Assistant,
1276            content: large_content.into(),
1277        });
1278
1279        let nonce = context_manager
1280            .persist_transaction(&transaction)
1281            .await
1282            .unwrap();
1283        let verification = context_manager
1284            .verify_persistence(&transaction, &nonce)
1285            .await
1286            .unwrap();
1287        assert!(verification);
1288    }
1289
1290    #[tokio::test]
1291    async fn verify_nonexistent_transaction() {
1292        let client = create_test_client().await;
1293        let collection = client
1294            .get_or_create_collection("test_transactions", None, None)
1295            .await
1296            .expect("Failed to create Chroma collection");
1297        let context_manager =
1298            ContextManager::new(collection).expect("Failed to create ContextManager");
1299        let transaction = create_test_transaction();
1300        let fake_nonce = TransactionID::generate().unwrap().to_string();
1301
1302        let verification = context_manager
1303            .verify_persistence(&transaction, &fake_nonce)
1304            .await
1305            .unwrap();
1306        assert!(!verification);
1307    }
1308
1309    #[tokio::test]
1310    async fn load_agent_single_context_single_transaction() {
1311        let client = create_test_client().await;
1312        let collection = client
1313            .get_or_create_collection("test_transactions", None, None)
1314            .await
1315            .expect("Failed to create Chroma collection");
1316        let context_manager =
1317            ContextManager::new(collection).expect("Failed to create ContextManager");
1318
1319        let agent_id = AgentID::generate().unwrap();
1320        let transaction = Transaction {
1321            agent_id,
1322            context_seq_no: 1,
1323            transaction_seq_no: 1,
1324            msgs: vec![MessageParam {
1325                role: MessageRole::User,
1326                content: "Test message for load_agent".into(),
1327            }],
1328            writes: vec![],
1329        };
1330
1331        // Persist the transaction first
1332        let _nonce = context_manager
1333            .persist_transaction(&transaction)
1334            .await
1335            .unwrap();
1336
1337        // Load the agent data
1338        let agent_data = context_manager.load_agent(agent_id).await.unwrap();
1339
1340        // Verify the loaded data
1341        assert_eq!(agent_data.agent_id, agent_id);
1342        assert_eq!(agent_data.contexts.len(), 1);
1343
1344        let context = &agent_data.contexts[0];
1345        assert_eq!(context.agent_id, agent_id);
1346        assert_eq!(context.context_seq_no, 1);
1347        assert_eq!(context.transactions.len(), 1);
1348
1349        let loaded_transaction = &context.transactions[0];
1350        assert_eq!(loaded_transaction.agent_id, transaction.agent_id);
1351        assert_eq!(
1352            loaded_transaction.context_seq_no,
1353            transaction.context_seq_no
1354        );
1355        assert_eq!(
1356            loaded_transaction.transaction_seq_no,
1357            transaction.transaction_seq_no
1358        );
1359        assert_eq!(loaded_transaction.msgs.len(), transaction.msgs.len());
1360    }
1361
1362    #[tokio::test]
1363    async fn load_agent_multiple_contexts_multiple_transactions() {
1364        let client = create_test_client().await;
1365        let collection = client
1366            .get_or_create_collection("test_transactions", None, None)
1367            .await
1368            .expect("Failed to create Chroma collection");
1369        let context_manager =
1370            ContextManager::new(collection).expect("Failed to create ContextManager");
1371
1372        let agent_id = AgentID::generate().unwrap();
1373
1374        // Create transactions in different contexts
1375        let transactions = vec![
1376            Transaction {
1377                agent_id,
1378                context_seq_no: 1,
1379                transaction_seq_no: 1,
1380                msgs: vec![MessageParam {
1381                    role: MessageRole::User,
1382                    content: "Context 1, Transaction 1".into(),
1383                }],
1384                writes: vec![],
1385            },
1386            Transaction {
1387                agent_id,
1388                context_seq_no: 1,
1389                transaction_seq_no: 2,
1390                msgs: vec![MessageParam {
1391                    role: MessageRole::Assistant,
1392                    content: "Context 1, Transaction 2".into(),
1393                }],
1394                writes: vec![],
1395            },
1396            Transaction {
1397                agent_id,
1398                context_seq_no: 2,
1399                transaction_seq_no: 1,
1400                msgs: vec![MessageParam {
1401                    role: MessageRole::User,
1402                    content: "Context 2, Transaction 1".into(),
1403                }],
1404                writes: vec![],
1405            },
1406        ];
1407
1408        // Persist all transactions
1409        for transaction in &transactions {
1410            let _nonce = context_manager
1411                .persist_transaction(transaction)
1412                .await
1413                .unwrap();
1414        }
1415
1416        // Load the agent data
1417        let agent_data = context_manager.load_agent(agent_id).await.unwrap();
1418
1419        // Verify the loaded data structure
1420        assert_eq!(agent_data.agent_id, agent_id);
1421        assert_eq!(agent_data.contexts.len(), 2);
1422
1423        // Check context 1
1424        let context1 = &agent_data.contexts[0];
1425        assert_eq!(context1.context_seq_no, 1);
1426        assert_eq!(context1.transactions.len(), 2);
1427        assert_eq!(context1.transactions[0].transaction_seq_no, 1);
1428        assert_eq!(context1.transactions[1].transaction_seq_no, 2);
1429
1430        // Check context 2
1431        let context2 = &agent_data.contexts[1];
1432        assert_eq!(context2.context_seq_no, 2);
1433        assert_eq!(context2.transactions.len(), 1);
1434        assert_eq!(context2.transactions[0].transaction_seq_no, 1);
1435
1436        // Test helper methods
1437        let all_transactions = agent_data.all_transactions();
1438        assert_eq!(all_transactions.len(), 3);
1439
1440        let latest_context = agent_data.latest_context().unwrap();
1441        assert_eq!(latest_context.context_seq_no, 2);
1442
1443        let specific_context = agent_data.get_context(1).unwrap();
1444        assert_eq!(specific_context.transactions.len(), 2);
1445    }
1446
1447    #[tokio::test]
1448    async fn load_agent_with_chunked_transactions() {
1449        let client = create_test_client().await;
1450        let collection = client
1451            .get_or_create_collection("test_transactions", None, None)
1452            .await
1453            .expect("Failed to create Chroma collection");
1454        let context_manager =
1455            ContextManager::new(collection).expect("Failed to create ContextManager");
1456
1457        let agent_id = AgentID::generate().unwrap();
1458
1459        // Create a large transaction that will be chunked
1460        let large_content = "x".repeat(crate::CHUNK_SIZE_LIMIT * 2);
1461        let transaction = Transaction {
1462            agent_id,
1463            context_seq_no: 1,
1464            transaction_seq_no: 1,
1465            msgs: vec![MessageParam {
1466                role: MessageRole::User,
1467                content: large_content.into(),
1468            }],
1469            writes: vec![],
1470        };
1471
1472        // Persist the transaction (this will create multiple chunks)
1473        let _nonce = context_manager
1474            .persist_transaction(&transaction)
1475            .await
1476            .unwrap();
1477
1478        // Load the agent data
1479        let agent_data = context_manager.load_agent(agent_id).await.unwrap();
1480
1481        // Verify the transaction was properly reassembled
1482        assert_eq!(agent_data.contexts.len(), 1);
1483        let context = &agent_data.contexts[0];
1484        assert_eq!(context.transactions.len(), 1);
1485
1486        let loaded_transaction = &context.transactions[0];
1487        assert_eq!(loaded_transaction.msgs.len(), transaction.msgs.len());
1488        assert_eq!(
1489            loaded_transaction.msgs[0].content,
1490            transaction.msgs[0].content
1491        );
1492    }
1493
1494    #[tokio::test]
1495    async fn load_nonexistent_agent() {
1496        let client = create_test_client().await;
1497        let collection = client
1498            .get_or_create_collection("test_transactions", None, None)
1499            .await
1500            .expect("Failed to create Chroma collection");
1501        let context_manager =
1502            ContextManager::new(collection).expect("Failed to create ContextManager");
1503
1504        let nonexistent_agent_id = AgentID::generate().unwrap();
1505
1506        // Load data for agent that doesn't exist
1507        let agent_data = context_manager
1508            .load_agent(nonexistent_agent_id)
1509            .await
1510            .unwrap();
1511
1512        // Should return empty but valid agent data
1513        assert_eq!(agent_data.agent_id, nonexistent_agent_id);
1514        assert!(agent_data.contexts.is_empty());
1515        assert!(agent_data.all_transactions().is_empty());
1516        assert!(agent_data.latest_context().is_none());
1517    }
1518
1519    #[tokio::test]
1520    async fn fluent_transaction_building_next_transaction() {
1521        let client = create_test_client().await;
1522        let collection = client
1523            .get_or_create_collection("test_transactions", None, None)
1524            .await
1525            .expect("Failed to create Chroma collection");
1526        let context_manager =
1527            ContextManager::new(collection).expect("Failed to create ContextManager");
1528
1529        let agent_id = AgentID::generate().unwrap();
1530
1531        // Create initial transaction
1532        let transaction = Transaction {
1533            agent_id,
1534            context_seq_no: 1,
1535            transaction_seq_no: 1,
1536            msgs: vec![MessageParam {
1537                role: MessageRole::User,
1538                content: "Initial message".into(),
1539            }],
1540            writes: vec![],
1541        };
1542
1543        context_manager
1544            .persist_transaction(&transaction)
1545            .await
1546            .unwrap();
1547
1548        // Load agent data
1549        let mut agent_data = context_manager.load_agent(agent_id).await.unwrap();
1550
1551        // Use fluent API to add next transaction
1552        let nonce = agent_data
1553            .next_transaction(&context_manager)
1554            .message(MessageParam {
1555                role: MessageRole::Assistant,
1556                content: "Response message".into(),
1557            })
1558            .save()
1559            .await
1560            .unwrap();
1561
1562        assert!(!nonce.is_empty());
1563
1564        // Verify the agent data was updated
1565        assert_eq!(agent_data.contexts.len(), 1);
1566        let context = &agent_data.contexts[0];
1567        assert_eq!(context.transactions.len(), 2);
1568        assert_eq!(context.transactions[1].transaction_seq_no, 2);
1569        assert_eq!(context.transactions[1].msgs.len(), 1);
1570
1571        // Verify it was persisted to Chroma
1572        let reloaded_data = context_manager.load_agent(agent_id).await.unwrap();
1573        assert_eq!(reloaded_data.contexts[0].transactions.len(), 2);
1574    }
1575
1576    #[tokio::test]
1577    async fn fluent_transaction_building_new_context() {
1578        let client = create_test_client().await;
1579        let collection = client
1580            .get_or_create_collection("test_transactions", None, None)
1581            .await
1582            .expect("Failed to create Chroma collection");
1583        let context_manager =
1584            ContextManager::new(collection).expect("Failed to create ContextManager");
1585
1586        let agent_id = AgentID::generate().unwrap();
1587
1588        // Create initial transaction in context 1
1589        let transaction = Transaction {
1590            agent_id,
1591            context_seq_no: 1,
1592            transaction_seq_no: 1,
1593            msgs: vec![MessageParam {
1594                role: MessageRole::User,
1595                content: "Initial message".into(),
1596            }],
1597            writes: vec![],
1598        };
1599
1600        context_manager
1601            .persist_transaction(&transaction)
1602            .await
1603            .unwrap();
1604
1605        // Load agent data
1606        let mut agent_data = context_manager.load_agent(agent_id).await.unwrap();
1607
1608        // Use fluent API to create transaction in new context
1609        let nonce = agent_data
1610            .new_context(&context_manager)
1611            .message(MessageParam {
1612                role: MessageRole::User,
1613                content: "New context message".into(),
1614            })
1615            .save()
1616            .await
1617            .unwrap();
1618
1619        assert!(!nonce.is_empty());
1620
1621        // Verify the agent data was updated with new context
1622        assert_eq!(agent_data.contexts.len(), 2);
1623        let new_context = &agent_data.contexts[1];
1624        assert_eq!(new_context.context_seq_no, 2);
1625        assert_eq!(new_context.transactions.len(), 1);
1626        assert_eq!(new_context.transactions[0].transaction_seq_no, 1);
1627
1628        // Verify it was persisted to Chroma
1629        let reloaded_data = context_manager.load_agent(agent_id).await.unwrap();
1630        assert_eq!(reloaded_data.contexts.len(), 2);
1631        assert_eq!(reloaded_data.contexts[1].context_seq_no, 2);
1632    }
1633
1634    #[tokio::test]
1635    async fn fluent_transaction_building_with_file_writes() {
1636        let client = create_test_client().await;
1637        let collection = client
1638            .get_or_create_collection("test_transactions", None, None)
1639            .await
1640            .expect("Failed to create Chroma collection");
1641        let context_manager =
1642            ContextManager::new(collection).expect("Failed to create ContextManager");
1643
1644        let agent_id = AgentID::generate().unwrap();
1645        let mount_id = crate::MountID::generate().unwrap();
1646
1647        // Start with empty agent
1648        let mut agent_data = AgentData {
1649            agent_id,
1650            contexts: vec![],
1651        };
1652
1653        // Use fluent API to create transaction with messages and file writes
1654        let nonce = agent_data
1655            .next_transaction(&context_manager)
1656            .message(MessageParam {
1657                role: MessageRole::User,
1658                content: "Create some files".into(),
1659            })
1660            .write_file(mount_id, "/test.txt", "Hello, world!")
1661            .unwrap()
1662            .write_file(mount_id, "/config.json", r#"{"setting": "value"}"#)
1663            .unwrap()
1664            .save()
1665            .await
1666            .unwrap();
1667
1668        assert!(!nonce.is_empty());
1669
1670        // Verify the transaction was created with file writes
1671        assert_eq!(agent_data.contexts.len(), 1);
1672        let context = &agent_data.contexts[0];
1673        assert_eq!(context.transactions.len(), 1);
1674        let transaction = &context.transactions[0];
1675        assert_eq!(transaction.writes.len(), 2);
1676        assert_eq!(transaction.writes[0].path, "/test.txt");
1677        assert_eq!(transaction.writes[0].data, "Hello, world!");
1678        assert_eq!(transaction.writes[1].path, "/config.json");
1679
1680        // Verify persistence
1681        let reloaded_data = context_manager.load_agent(agent_id).await.unwrap();
1682        let reloaded_transaction = &reloaded_data.contexts[0].transactions[0];
1683        assert_eq!(reloaded_transaction.writes.len(), 2);
1684    }
1685
1686    #[tokio::test]
1687    async fn fluent_transaction_building_multiple_messages() {
1688        let client = create_test_client().await;
1689        let collection = client
1690            .get_or_create_collection("test_transactions", None, None)
1691            .await
1692            .expect("Failed to create Chroma collection");
1693        let context_manager =
1694            ContextManager::new(collection).expect("Failed to create ContextManager");
1695
1696        let agent_id = AgentID::generate().unwrap();
1697
1698        // Start with empty agent
1699        let mut agent_data = AgentData {
1700            agent_id,
1701            contexts: vec![],
1702        };
1703
1704        let messages = vec![
1705            MessageParam {
1706                role: MessageRole::User,
1707                content: "First message".into(),
1708            },
1709            MessageParam {
1710                role: MessageRole::Assistant,
1711                content: "Second message".into(),
1712            },
1713        ];
1714
1715        // Use fluent API with multiple messages
1716        let nonce = agent_data
1717            .next_transaction(&context_manager)
1718            .messages(messages.clone())
1719            .message(MessageParam {
1720                role: MessageRole::User,
1721                content: "Third message".into(),
1722            })
1723            .save()
1724            .await
1725            .unwrap();
1726
1727        assert!(!nonce.is_empty());
1728
1729        // Verify all messages were added
1730        let context = &agent_data.contexts[0];
1731        let transaction = &context.transactions[0];
1732        assert_eq!(transaction.msgs.len(), 3);
1733
1734        // Verify roles are correct
1735        assert_eq!(transaction.msgs[0].role, MessageRole::User);
1736        assert_eq!(transaction.msgs[1].role, MessageRole::Assistant);
1737        assert_eq!(transaction.msgs[2].role, MessageRole::User);
1738
1739        // Verify content matches by directly comparing the content fields
1740        assert_eq!(transaction.msgs[0].content, messages[0].content);
1741        assert_eq!(transaction.msgs[1].content, messages[1].content);
1742    }
1743
1744    #[tokio::test]
1745    async fn fluent_transaction_building_build_without_save() {
1746        let client = create_test_client().await;
1747        let collection = client
1748            .get_or_create_collection("test_transactions", None, None)
1749            .await
1750            .expect("Failed to create Chroma collection");
1751        let context_manager =
1752            ContextManager::new(collection).expect("Failed to create ContextManager");
1753
1754        let agent_id = AgentID::generate().unwrap();
1755        let mut agent_data = AgentData {
1756            agent_id,
1757            contexts: vec![],
1758        };
1759
1760        // Build without saving
1761        let transaction = agent_data
1762            .next_transaction(&context_manager)
1763            .message(MessageParam {
1764                role: MessageRole::User,
1765                content: "Test message".into(),
1766            })
1767            .build();
1768
1769        // Verify transaction structure
1770        assert_eq!(transaction.agent_id, agent_id);
1771        assert_eq!(transaction.context_seq_no, 1);
1772        assert_eq!(transaction.transaction_seq_no, 1);
1773        assert_eq!(transaction.msgs.len(), 1);
1774
1775        // Verify agent_data was not modified (build() doesn't update it)
1776        assert!(agent_data.contexts.is_empty());
1777
1778        // Verify nothing was persisted
1779        let loaded_data = context_manager.load_agent(agent_id).await.unwrap();
1780        assert!(loaded_data.contexts.is_empty());
1781    }
1782
1783    #[tokio::test]
1784    async fn fluent_transaction_building_sequence_numbers() {
1785        let client = create_test_client().await;
1786        let collection = client
1787            .get_or_create_collection("test_transactions", None, None)
1788            .await
1789            .expect("Failed to create Chroma collection");
1790        let context_manager =
1791            ContextManager::new(collection).expect("Failed to create ContextManager");
1792
1793        let agent_id = AgentID::generate().unwrap();
1794        let mut agent_data = AgentData {
1795            agent_id,
1796            contexts: vec![],
1797        };
1798
1799        // First transaction - should be context 1, transaction 1
1800        let tx1 = agent_data
1801            .next_transaction(&context_manager)
1802            .message(MessageParam {
1803                role: MessageRole::User,
1804                content: "Message 1".into(),
1805            })
1806            .build();
1807
1808        assert_eq!(tx1.context_seq_no, 1);
1809        assert_eq!(tx1.transaction_seq_no, 1);
1810
1811        // Manually add to agent_data to simulate persistence
1812        agent_data.contexts.push(AgentContext {
1813            agent_id,
1814            context_seq_no: 1,
1815            transactions: vec![tx1],
1816        });
1817
1818        // Second transaction - should be context 1, transaction 2
1819        let tx2 = agent_data
1820            .next_transaction(&context_manager)
1821            .message(MessageParam {
1822                role: MessageRole::Assistant,
1823                content: "Message 2".into(),
1824            })
1825            .build();
1826
1827        assert_eq!(tx2.context_seq_no, 1);
1828        assert_eq!(tx2.transaction_seq_no, 2);
1829
1830        // New context - should be context 2, transaction 1
1831        let tx3 = agent_data
1832            .new_context(&context_manager)
1833            .message(MessageParam {
1834                role: MessageRole::User,
1835                content: "Message 3".into(),
1836            })
1837            .build();
1838
1839        assert_eq!(tx3.context_seq_no, 2);
1840        assert_eq!(tx3.transaction_seq_no, 1);
1841    }
1842
1843    #[tokio::test]
1844    async fn agent_data_file_reading() {
1845        let client = create_test_client().await;
1846
1847        client
1848            .heartbeat()
1849            .await
1850            .expect("Chroma heartbeat failed; ensure environment is configured");
1851
1852        let collection_name = format!("test_agent_file_reading_{}", rand::random::<u32>());
1853        let collection = client
1854            .get_or_create_collection(&collection_name, None, None)
1855            .await
1856            .unwrap();
1857
1858        let context_manager = ContextManager::new(collection).unwrap();
1859        let agent_id = AgentID::generate().unwrap();
1860        let mount_id = crate::MountID::generate().unwrap();
1861
1862        // Create initial transaction with some files
1863        let mut agent_data = AgentData {
1864            agent_id,
1865            contexts: Vec::new(),
1866        };
1867
1868        let _nonce = agent_data
1869            .new_context(&context_manager)
1870            .write_file(mount_id, "/file1.txt", "First file content")
1871            .unwrap()
1872            .write_file(mount_id, "/file2.txt", "Second file content")
1873            .unwrap()
1874            .write_file(mount_id, "/subdir/file3.txt", "Third file content")
1875            .unwrap()
1876            .save()
1877            .await
1878            .unwrap();
1879
1880        // Test get_file_content
1881        assert_eq!(
1882            agent_data.get_file_content(mount_id, "/file1.txt").unwrap(),
1883            Some("First file content".to_string())
1884        );
1885        assert_eq!(
1886            agent_data
1887                .get_file_content(mount_id, "/nonexistent.txt")
1888                .unwrap(),
1889            None
1890        );
1891
1892        // Test list_files
1893        let files = agent_data.list_files(mount_id);
1894        assert_eq!(files.len(), 3);
1895        assert!(files.contains(&"/file1.txt".to_string()));
1896        assert!(files.contains(&"/file2.txt".to_string()));
1897        assert!(files.contains(&"/subdir/file3.txt".to_string()));
1898
1899        // Test search_file_contents
1900        let matches = agent_data.search_file_contents(mount_id, "file").unwrap();
1901        assert_eq!(matches.len(), 3);
1902        assert!(matches.iter().any(|(path, _)| path == "/file1.txt"));
1903
1904        let matches = agent_data.search_file_contents(mount_id, "Third").unwrap();
1905        assert_eq!(matches.len(), 1);
1906        assert_eq!(matches[0].0, "/subdir/file3.txt");
1907    }
1908
1909    #[tokio::test]
1910    async fn transaction_builder_filesystem_methods() {
1911        let client = create_test_client().await;
1912
1913        client
1914            .heartbeat()
1915            .await
1916            .expect("Chroma heartbeat failed; ensure environment is configured");
1917
1918        let collection_name = format!("test_builder_fs_{}", rand::random::<u32>());
1919        let collection = client
1920            .get_or_create_collection(&collection_name, None, None)
1921            .await
1922            .unwrap();
1923
1924        let context_manager = ContextManager::new(collection).unwrap();
1925        let agent_id = AgentID::generate().unwrap();
1926        let mount_id = crate::MountID::generate().unwrap();
1927
1928        // Create initial transaction
1929        let mut agent_data = AgentData {
1930            agent_id,
1931            contexts: Vec::new(),
1932        };
1933
1934        let _nonce = agent_data
1935            .new_context(&context_manager)
1936            .write_file(mount_id, "/original.txt", "Original content")
1937            .unwrap()
1938            .save()
1939            .await
1940            .unwrap();
1941
1942        // Test TransactionBuilder filesystem methods
1943        let builder = agent_data.next_transaction(&context_manager);
1944
1945        // Test view_file
1946        assert_eq!(
1947            builder.view_file(mount_id, "/original.txt"),
1948            Some("Original content".to_string())
1949        );
1950        assert_eq!(builder.view_file(mount_id, "/nonexistent.txt"), None);
1951
1952        // Test list_files
1953        let files = builder.list_files(mount_id);
1954        assert_eq!(files.len(), 1);
1955        assert!(files.contains(&"/original.txt".to_string()));
1956
1957        // Test search_files
1958        let matches = builder.search_files(mount_id, "Original");
1959        assert_eq!(matches.len(), 1);
1960        assert_eq!(matches[0].0, "/original.txt");
1961
1962        // Test str_replace_file
1963        let builder = builder
1964            .str_replace_file(mount_id, "/original.txt", "Original", "Modified")
1965            .unwrap();
1966
1967        // Test get_buffered_content (should show the modified version)
1968        assert_eq!(
1969            builder
1970                .get_buffered_content(mount_id, "/original.txt")
1971                .unwrap(),
1972            Some("Modified content".to_string())
1973        );
1974
1975        // Test str_replace_file error cases
1976        let builder2 = agent_data.next_transaction(&context_manager);
1977        let result = builder2.str_replace_file(mount_id, "/nonexistent.txt", "old", "new");
1978        assert!(result.is_err());
1979        if let Err(error) = result {
1980            assert!(error.to_string().contains("not found"));
1981        }
1982
1983        let builder3 = agent_data.next_transaction(&context_manager);
1984        let result = builder3.str_replace_file(mount_id, "/original.txt", "nonexistent", "new");
1985        assert!(result.is_err());
1986        if let Err(error) = result {
1987            assert!(error.to_string().contains("not found"));
1988        }
1989
1990        // Test insert_file
1991        let builder4 = agent_data
1992            .next_transaction(&context_manager)
1993            .insert_file(mount_id, "/new_file.txt", "New file content")
1994            .unwrap();
1995
1996        assert_eq!(
1997            builder4
1998                .get_buffered_content(mount_id, "/new_file.txt")
1999                .unwrap(),
2000            Some("New file content".to_string())
2001        );
2002    }
2003
2004    #[tokio::test]
2005    async fn write_buffering_latest_wins() {
2006        let client = create_test_client().await;
2007
2008        client
2009            .heartbeat()
2010            .await
2011            .expect("Chroma heartbeat failed; ensure environment is configured");
2012
2013        let collection_name = format!("test_write_buffering_{}", rand::random::<u32>());
2014        let collection = client
2015            .get_or_create_collection(&collection_name, None, None)
2016            .await
2017            .unwrap();
2018
2019        let context_manager = ContextManager::new(collection).unwrap();
2020        let agent_id = AgentID::generate().unwrap();
2021        let mount_id = crate::MountID::generate().unwrap();
2022
2023        let mut agent_data = AgentData {
2024            agent_id,
2025            contexts: Vec::new(),
2026        };
2027
2028        // Test multiple writes to same file - latest should win
2029        let builder = agent_data
2030            .new_context(&context_manager)
2031            .write_file(mount_id, "/multi_write.txt", "First write")
2032            .unwrap()
2033            .write_file(mount_id, "/multi_write.txt", "Second write")
2034            .unwrap()
2035            .write_file(mount_id, "/multi_write.txt", "Third write")
2036            .unwrap();
2037
2038        // Test get_buffered_content returns the latest write
2039        assert_eq!(
2040            builder
2041                .get_buffered_content(mount_id, "/multi_write.txt")
2042                .unwrap(),
2043            Some("Third write".to_string())
2044        );
2045
2046        // Test write summary shows all writes
2047        let summary = builder.get_write_summary();
2048        assert_eq!(summary.len(), 3); // All three writes present
2049        assert!(
2050            summary
2051                .iter()
2052                .all(|(_, path, _)| path == "/multi_write.txt")
2053        );
2054
2055        // Save and verify the latest write persists
2056        let _nonce = builder.save().await.unwrap();
2057
2058        // Verify that reading from agent data returns the latest write
2059        assert_eq!(
2060            agent_data
2061                .get_file_content(mount_id, "/multi_write.txt")
2062                .unwrap(),
2063            Some("Third write".to_string())
2064        );
2065    }
2066
2067    #[tokio::test]
2068    async fn multiple_writes_same_file_appears_once() {
2069        let client = create_test_client().await;
2070
2071        client
2072            .heartbeat()
2073            .await
2074            .expect("Chroma heartbeat failed; ensure environment is configured");
2075
2076        let collection_name = format!("test_multiple_writes_{}", rand::random::<u32>());
2077        let collection = client
2078            .get_or_create_collection(&collection_name, None, None)
2079            .await
2080            .unwrap();
2081
2082        let context_manager = ContextManager::new(collection).unwrap();
2083        let agent_id = AgentID::generate().unwrap();
2084        let mount_id = crate::MountID::generate().unwrap();
2085
2086        let mut agent_data = AgentData {
2087            agent_id,
2088            contexts: Vec::new(),
2089        };
2090
2091        // Write the same file multiple times across different transactions
2092        let _nonce1 = agent_data
2093            .new_context(&context_manager)
2094            .write_file(mount_id, "/config.txt", "Version 1 content")
2095            .unwrap()
2096            .write_file(mount_id, "/other.txt", "Other file content")
2097            .unwrap()
2098            .save()
2099            .await
2100            .unwrap();
2101
2102        let _nonce2 = agent_data
2103            .next_transaction(&context_manager)
2104            .write_file(mount_id, "/config.txt", "Version 2 content")
2105            .unwrap()
2106            .save()
2107            .await
2108            .unwrap();
2109
2110        let _nonce3 = agent_data
2111            .next_transaction(&context_manager)
2112            .write_file(mount_id, "/config.txt", "Version 3 final content")
2113            .unwrap()
2114            .save()
2115            .await
2116            .unwrap();
2117
2118        // Test 1: list_files should show each file only once, despite multiple writes
2119        let files = agent_data.list_files(mount_id);
2120        assert_eq!(files.len(), 2); // Should have "/config.txt" and "/other.txt"
2121        assert!(files.contains(&"/config.txt".to_string()));
2122        assert!(files.contains(&"/other.txt".to_string()));
2123
2124        // Count occurrences - each file should appear exactly once
2125        let config_count = files.iter().filter(|&f| f == "/config.txt").count();
2126        let other_count = files.iter().filter(|&f| f == "/other.txt").count();
2127        assert_eq!(
2128            config_count, 1,
2129            "config.txt should appear exactly once in file list"
2130        );
2131        assert_eq!(
2132            other_count, 1,
2133            "other.txt should appear exactly once in file list"
2134        );
2135
2136        // Test 2: get_file_content should return the most recent version
2137        let content = agent_data
2138            .get_file_content(mount_id, "/config.txt")
2139            .unwrap();
2140        assert_eq!(content, Some("Version 3 final content".to_string()));
2141
2142        let other_content = agent_data.get_file_content(mount_id, "/other.txt").unwrap();
2143        assert_eq!(other_content, Some("Other file content".to_string()));
2144
2145        // Test 3: search_file_contents should find the file only once with latest content
2146        let matches = agent_data.search_file_contents(mount_id, "final").unwrap();
2147        assert_eq!(matches.len(), 1, "Should find config.txt exactly once");
2148        assert_eq!(matches[0].0, "/config.txt");
2149        assert!(matches[0].1[0].contains("Version 3 final content"));
2150
2151        // Test 4: search should not find old versions
2152        let old_matches = agent_data
2153            .search_file_contents(mount_id, "Version 1")
2154            .unwrap();
2155        assert_eq!(
2156            old_matches.len(),
2157            0,
2158            "Should not find old Version 1 content"
2159        );
2160
2161        let old_matches2 = agent_data
2162            .search_file_contents(mount_id, "Version 2")
2163            .unwrap();
2164        assert_eq!(
2165            old_matches2.len(),
2166            0,
2167            "Should not find old Version 2 content"
2168        );
2169
2170        // Test 5: TransactionBuilder should also handle this correctly with pending writes
2171        let builder = agent_data
2172            .next_transaction(&context_manager)
2173            .write_file(mount_id, "/config.txt", "Pending version")
2174            .unwrap();
2175
2176        // TransactionBuilder's list_files should still show each file once
2177        let builder_files = builder.list_files(mount_id);
2178        let config_count_builder = builder_files.iter().filter(|&f| f == "/config.txt").count();
2179        assert_eq!(
2180            config_count_builder, 1,
2181            "TransactionBuilder should show config.txt exactly once"
2182        );
2183
2184        // get_buffered_content should return the pending write (most recent)
2185        assert_eq!(
2186            builder
2187                .get_buffered_content(mount_id, "/config.txt")
2188                .unwrap(),
2189            Some("Pending version".to_string())
2190        );
2191
2192        // search_files should find the pending version
2193        let builder_matches = builder.search_files(mount_id, "Pending");
2194        assert_eq!(
2195            builder_matches.len(),
2196            1,
2197            "Should find file with pending write exactly once"
2198        );
2199        assert_eq!(builder_matches[0].0, "/config.txt");
2200    }
2201}