Skip to main content

xchecker_runner/claude/
types.rs

1use crate::ndjson::NdjsonResult;
2use crate::types::RunnerMode;
3
4/// Configuration options for WSL execution
5#[derive(Debug, Clone, Default)]
6pub struct WslOptions {
7    /// Optional specific WSL distro to use (e.g., "Ubuntu-22.04")
8    pub distro: Option<String>,
9    /// Optional absolute path to claude binary in WSL (defaults to "claude")
10    pub claude_path: Option<String>,
11}
12
13/// Configuration for output buffering
14#[derive(Debug, Clone)]
15pub struct BufferConfig {
16    /// Maximum bytes to buffer for stdout (default: 2 MiB)
17    pub stdout_cap_bytes: usize,
18    /// Maximum bytes to buffer for stderr (default: 256 KiB)
19    pub stderr_cap_bytes: usize,
20    /// Maximum bytes for stderr in receipts after redaction (default: 2048)
21    #[allow(dead_code)] // Buffer management metadata
22    pub stderr_receipt_cap_bytes: usize,
23}
24
25impl Default for BufferConfig {
26    fn default() -> Self {
27        Self {
28            stdout_cap_bytes: 2 * 1024 * 1024, // 2 MiB
29            stderr_cap_bytes: 256 * 1024,      // 256 KiB
30            stderr_receipt_cap_bytes: 2048,    // 2048 bytes
31        }
32    }
33}
34
35/// Response from Claude CLI execution
36#[derive(Debug)]
37pub struct ClaudeResponse {
38    /// Standard output from Claude CLI (raw, may be truncated if > `stdout_cap_bytes`)
39    pub stdout: String,
40    /// Standard error from Claude CLI (may be truncated if > `stderr_cap_bytes`)
41    pub stderr: String,
42    /// Exit code from Claude CLI process
43    pub exit_code: i32,
44    /// The runner mode that was actually used
45    pub runner_used: RunnerMode,
46    /// The WSL distro that was used (if applicable)
47    pub runner_distro: Option<String>,
48    /// Whether the execution timed out
49    pub timed_out: bool,
50    /// Parsed NDJSON result from stdout
51    pub ndjson_result: NdjsonResult,
52    /// Whether stdout was truncated due to buffer limits
53    #[allow(dead_code)] // Truncation tracking metadata
54    pub stdout_truncated: bool,
55    /// Whether stderr was truncated due to buffer limits
56    #[allow(dead_code)] // Truncation tracking metadata
57    pub stderr_truncated: bool,
58    /// Total bytes written to stdout (including truncated)
59    #[allow(dead_code)] // Buffer management metadata
60    pub stdout_total_bytes: usize,
61    /// Total bytes written to stderr (including truncated)
62    #[allow(dead_code)] // Buffer management metadata
63    pub stderr_total_bytes: usize,
64}
65
66impl ClaudeResponse {
67    /// Get stderr truncated to receipt size limit (2048 bytes by default)
68    ///
69    /// This should be called AFTER redaction to ensure the final size is <= 2048 bytes.
70    /// The caller is responsible for applying redaction before calling this method.
71    #[must_use]
72    #[allow(dead_code)] // Runner utility method for receipt generation
73    pub fn stderr_for_receipt(&self, max_bytes: usize) -> String {
74        if self.stderr.len() <= max_bytes {
75            self.stderr.clone()
76        } else {
77            // Take the last max_bytes characters (tail of stderr)
78            let bytes = self.stderr.as_bytes();
79            let start = bytes.len().saturating_sub(max_bytes);
80            String::from_utf8_lossy(&bytes[start..]).to_string()
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::{BufferConfig, ClaudeResponse, WslOptions};
88    use crate::ndjson::NdjsonResult;
89    use crate::types::RunnerMode;
90
91    #[test]
92    fn test_wsl_options_default() {
93        let options = WslOptions::default();
94        assert!(options.distro.is_none());
95        assert!(options.claude_path.is_none());
96    }
97
98    // Buffer configuration tests
99
100    #[test]
101    fn test_buffer_config_default() {
102        let config = BufferConfig::default();
103        assert_eq!(config.stdout_cap_bytes, 2 * 1024 * 1024); // 2 MiB
104        assert_eq!(config.stderr_cap_bytes, 256 * 1024); // 256 KiB
105        assert_eq!(config.stderr_receipt_cap_bytes, 2048); // 2048 bytes
106    }
107
108    #[test]
109    fn test_buffer_config_custom() {
110        let config = BufferConfig {
111            stdout_cap_bytes: 1024,
112            stderr_cap_bytes: 512,
113            stderr_receipt_cap_bytes: 256,
114        };
115        assert_eq!(config.stdout_cap_bytes, 1024);
116        assert_eq!(config.stderr_cap_bytes, 512);
117        assert_eq!(config.stderr_receipt_cap_bytes, 256);
118    }
119
120    #[test]
121    fn test_claude_response_stderr_for_receipt_no_truncation() {
122        let response = ClaudeResponse {
123            stdout: String::new(),
124            stderr: "Short error message".to_string(),
125            exit_code: 0,
126            runner_used: RunnerMode::Native,
127            runner_distro: None,
128            timed_out: false,
129            ndjson_result: NdjsonResult::NoValidJson {
130                tail_excerpt: String::new(),
131            },
132            stdout_truncated: false,
133            stderr_truncated: false,
134            stdout_total_bytes: 0,
135            stderr_total_bytes: 20,
136        };
137
138        let stderr_receipt = response.stderr_for_receipt(2048);
139        assert_eq!(stderr_receipt, "Short error message");
140    }
141
142    #[test]
143    fn test_claude_response_stderr_for_receipt_with_truncation() {
144        let long_stderr = "x".repeat(3000);
145        let response = ClaudeResponse {
146            stdout: String::new(),
147            stderr: long_stderr,
148            exit_code: 0,
149            runner_used: RunnerMode::Native,
150            runner_distro: None,
151            timed_out: false,
152            ndjson_result: NdjsonResult::NoValidJson {
153                tail_excerpt: String::new(),
154            },
155            stdout_truncated: false,
156            stderr_truncated: false,
157            stdout_total_bytes: 0,
158            stderr_total_bytes: 3000,
159        };
160
161        let stderr_receipt = response.stderr_for_receipt(2048);
162        assert_eq!(stderr_receipt.len(), 2048);
163        // Should be the last 2048 characters
164        assert_eq!(stderr_receipt, "x".repeat(2048));
165    }
166
167    #[test]
168    fn test_claude_response_stderr_for_receipt_exact_limit() {
169        let stderr = "x".repeat(2048);
170        let response = ClaudeResponse {
171            stdout: String::new(),
172            stderr: stderr.clone(),
173            exit_code: 0,
174            runner_used: RunnerMode::Native,
175            runner_distro: None,
176            timed_out: false,
177            ndjson_result: NdjsonResult::NoValidJson {
178                tail_excerpt: String::new(),
179            },
180            stdout_truncated: false,
181            stderr_truncated: false,
182            stdout_total_bytes: 0,
183            stderr_total_bytes: 2048,
184        };
185
186        let stderr_receipt = response.stderr_for_receipt(2048);
187        assert_eq!(stderr_receipt.len(), 2048);
188        assert_eq!(stderr_receipt, stderr);
189    }
190
191    #[test]
192    fn test_claude_response_stderr_for_receipt_custom_limit() {
193        let stderr = "Hello, world! This is a test message.".to_string();
194        let response = ClaudeResponse {
195            stdout: String::new(),
196            stderr: stderr.clone(),
197            exit_code: 0,
198            runner_used: RunnerMode::Native,
199            runner_distro: None,
200            timed_out: false,
201            ndjson_result: NdjsonResult::NoValidJson {
202                tail_excerpt: String::new(),
203            },
204            stdout_truncated: false,
205            stderr_truncated: false,
206            stdout_total_bytes: 0,
207            stderr_total_bytes: stderr.len(),
208        };
209
210        let stderr_receipt = response.stderr_for_receipt(10);
211        assert_eq!(stderr_receipt.len(), 10);
212        // Should be the last 10 bytes
213        assert_eq!(stderr_receipt, "t message.");
214    }
215}