agtrace_testing/
fixtures.rs

1//! Fixtures for sample data generation and placement.
2//!
3//! Provides utilities to:
4//! - Copy sample files to test environments
5//! - Modify sample data for isolation (e.g., cwd, sessionId)
6//! - Generate provider-specific directory structures
7
8use anyhow::Result;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12/// Sample file manager for test data.
13pub struct SampleFiles {
14    samples_dir: PathBuf,
15}
16
17impl Default for SampleFiles {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl SampleFiles {
24    /// Create a new sample file manager.
25    ///
26    /// Assumes samples are in `crates/agtrace-providers/tests/samples/`.
27    pub fn new() -> Self {
28        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
29        let samples_dir = manifest_dir
30            .parent()
31            .unwrap()
32            .join("agtrace-providers/tests/samples");
33
34        Self { samples_dir }
35    }
36
37    /// Copy a sample file to a destination.
38    pub fn copy_to(&self, sample_name: &str, dest: &Path) -> Result<()> {
39        let source = self.samples_dir.join(sample_name);
40        fs::copy(source, dest)?;
41        Ok(())
42    }
43
44    /// Copy a sample file to a Claude-encoded project directory.
45    ///
46    /// Claude encodes project paths like: `/Users/foo/bar` -> `-Users-foo-bar`
47    pub fn copy_to_project(
48        &self,
49        sample_name: &str,
50        dest_name: &str,
51        project_dir: &str,
52        log_root: &Path,
53    ) -> Result<()> {
54        let source = self.samples_dir.join(sample_name);
55
56        // Encode project directory (Claude format)
57        let encoded = project_dir
58            .replace(['/', '.'], "-")
59            .trim_start_matches('-')
60            .to_string();
61        let encoded_dir = format!("-{}", encoded);
62
63        let project_log_dir = log_root.join(encoded_dir);
64        fs::create_dir_all(&project_log_dir)?;
65
66        let dest = project_log_dir.join(dest_name);
67        fs::copy(source, dest)?;
68        Ok(())
69    }
70
71    /// Copy a sample file with cwd and sessionId replacement.
72    ///
73    /// This creates isolated test sessions by:
74    /// 1. Replacing the embedded `cwd` field with `target_project_dir` (canonicalized)
75    /// 2. Generating a unique `sessionId` based on project dir + filename
76    /// 3. Using provider-specific directory encoding via the provider adapter
77    pub fn copy_to_project_with_cwd(
78        &self,
79        sample_name: &str,
80        dest_name: &str,
81        target_project_dir: &str,
82        log_root: &Path,
83        provider_adapter: &agtrace_providers::ProviderAdapter,
84    ) -> Result<()> {
85        let source = self.samples_dir.join(sample_name);
86
87        // Canonicalize target_project_dir to match project_hash_from_root behavior
88        let canonical_project_dir = Path::new(target_project_dir)
89            .canonicalize()
90            .unwrap_or_else(|_| Path::new(target_project_dir).to_path_buf());
91        let canonical_str = canonical_project_dir.to_string_lossy();
92
93        // Use provider-specific directory encoding from the discovery trait
94        let project_log_dir = if let Some(provider_subdir) = provider_adapter
95            .discovery
96            .resolve_log_root(&canonical_project_dir)
97        {
98            // Provider uses project-specific subdirectory (e.g., Gemini uses hash)
99            log_root.join(provider_subdir)
100        } else {
101            // Provider uses flat structure with encoded project names (e.g., Claude)
102            let encoded = target_project_dir
103                .replace(['/', '.'], "-")
104                .trim_start_matches('-')
105                .to_string();
106            let encoded_dir = format!("-{}", encoded);
107            log_root.join(encoded_dir)
108        };
109
110        fs::create_dir_all(&project_log_dir)?;
111
112        let dest = project_log_dir.join(dest_name);
113
114        // Read and modify content
115        let content = fs::read_to_string(&source)?;
116
117        // Replace cwd field with canonicalized path (Claude format)
118        let mut modified_content = content.replace(
119            r#""cwd":"/Users/test_user/agent-sample""#,
120            &format!(r#""cwd":"{}""#, canonical_str),
121        );
122
123        // Replace projectHash field (Gemini format)
124        // Calculate the correct project hash from the canonicalized path
125        let project_hash = agtrace_types::project_hash_from_root(&canonical_str);
126        modified_content = modified_content.replace(
127            r#""projectHash": "9126eddec7f67e038794657b4d517dd9cb5226468f30b5ee7296c27d65e84fde""#,
128            &format!(r#""projectHash": "{}""#, project_hash),
129        );
130
131        // Generate unique sessionId
132        let new_session_id = generate_session_id(target_project_dir, dest_name);
133
134        // Replace sessionId (Claude: 7f2abd2d-7cfc-4447-9ddd-3ca8d14e02e9)
135        modified_content =
136            modified_content.replace("7f2abd2d-7cfc-4447-9ddd-3ca8d14e02e9", &new_session_id);
137
138        // Replace sessionId (Gemini: f0a689a6-b0ac-407f-afcc-4fafa9e14e8a)
139        modified_content =
140            modified_content.replace("f0a689a6-b0ac-407f-afcc-4fafa9e14e8a", &new_session_id);
141
142        fs::write(dest, modified_content)?;
143        Ok(())
144    }
145}
146
147/// Generate a deterministic session ID based on project directory and filename.
148fn generate_session_id(project_dir: &str, filename: &str) -> String {
149    use sha2::{Digest, Sha256};
150
151    let mut hasher = Sha256::new();
152    hasher.update(project_dir.as_bytes());
153    hasher.update(filename.as_bytes());
154    let hash = hasher.finalize();
155
156    format!(
157        "test-session-{:016x}",
158        u64::from_be_bytes([
159            hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]
160        ])
161    )
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_generate_session_id_deterministic() {
170        let id1 = generate_session_id("/Users/test/project-a", "session1.jsonl");
171        let id2 = generate_session_id("/Users/test/project-a", "session1.jsonl");
172        assert_eq!(id1, id2);
173    }
174
175    #[test]
176    fn test_generate_session_id_unique() {
177        let id1 = generate_session_id("/Users/test/project-a", "session1.jsonl");
178        let id2 = generate_session_id("/Users/test/project-b", "session1.jsonl");
179        assert_ne!(id1, id2);
180    }
181}