grok-cli-acp 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +42 -0
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/rust.yml +22 -0
- package/.grok/.env.example +85 -0
- package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
- package/.grok/ENV_CONFIG_GUIDE.md +173 -0
- package/.grok/QUICK_REFERENCE.md +180 -0
- package/.grok/README.md +104 -0
- package/.grok/TESTING_GUIDE.md +393 -0
- package/CHANGELOG.md +465 -0
- package/CODE_REVIEW_SUMMARY.md +414 -0
- package/COMPLETE_FIX_SUMMARY.md +415 -0
- package/CONFIGURATION.md +489 -0
- package/CONTEXT_FILES_GUIDE.md +419 -0
- package/CONTRIBUTING.md +55 -0
- package/CURSOR_POSITION_FIX.md +206 -0
- package/Cargo.toml +88 -0
- package/ERROR_HANDLING_REPORT.md +361 -0
- package/FINAL_FIX_SUMMARY.md +462 -0
- package/FIXES.md +37 -0
- package/FIXES_SUMMARY.md +87 -0
- package/GROK_API_MIGRATION_SUMMARY.md +111 -0
- package/LICENSE +22 -0
- package/MIGRATION_TO_GROK_API.md +223 -0
- package/README.md +504 -0
- package/REVIEW_COMPLETE.md +416 -0
- package/REVIEW_QUICK_REFERENCE.md +173 -0
- package/SECURITY.md +463 -0
- package/SECURITY_AUDIT.md +661 -0
- package/SETUP.md +287 -0
- package/TESTING_TOOLS.md +88 -0
- package/TESTING_TOOL_EXECUTION.md +239 -0
- package/TOOL_EXECUTION_FIX.md +491 -0
- package/VERIFICATION_CHECKLIST.md +419 -0
- package/docs/API.md +74 -0
- package/docs/CHAT_LOGGING.md +39 -0
- package/docs/CURSOR_FIX_DEMO.md +306 -0
- package/docs/ERROR_HANDLING_GUIDE.md +547 -0
- package/docs/FILE_OPERATIONS.md +449 -0
- package/docs/INTERACTIVE.md +401 -0
- package/docs/PROJECT_CREATION_GUIDE.md +570 -0
- package/docs/QUICKSTART.md +378 -0
- package/docs/QUICK_REFERENCE.md +691 -0
- package/docs/RELEASE_NOTES_0.1.2.md +240 -0
- package/docs/TOOLS.md +459 -0
- package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
- package/docs/ZED_INTEGRATION.md +371 -0
- package/docs/extensions.md +464 -0
- package/docs/settings.md +293 -0
- package/examples/extensions/logging-hook/README.md +91 -0
- package/examples/extensions/logging-hook/extension.json +22 -0
- package/package.json +30 -0
- package/scripts/test_acp.py +252 -0
- package/scripts/test_acp.sh +143 -0
- package/scripts/test_acp_simple.sh +72 -0
- package/src/acp/mod.rs +741 -0
- package/src/acp/protocol.rs +323 -0
- package/src/acp/security.rs +298 -0
- package/src/acp/tools.rs +697 -0
- package/src/bin/banner_demo.rs +216 -0
- package/src/bin/docgen.rs +18 -0
- package/src/bin/installer.rs +217 -0
- package/src/cli/app.rs +310 -0
- package/src/cli/commands/acp.rs +721 -0
- package/src/cli/commands/chat.rs +485 -0
- package/src/cli/commands/code.rs +513 -0
- package/src/cli/commands/config.rs +394 -0
- package/src/cli/commands/health.rs +442 -0
- package/src/cli/commands/history.rs +421 -0
- package/src/cli/commands/mod.rs +14 -0
- package/src/cli/commands/settings.rs +1384 -0
- package/src/cli/mod.rs +166 -0
- package/src/config/mod.rs +2212 -0
- package/src/display/ascii_art.rs +139 -0
- package/src/display/banner.rs +289 -0
- package/src/display/components/input.rs +323 -0
- package/src/display/components/mod.rs +2 -0
- package/src/display/components/settings_list.rs +306 -0
- package/src/display/interactive.rs +1255 -0
- package/src/display/mod.rs +62 -0
- package/src/display/terminal.rs +42 -0
- package/src/display/tips.rs +316 -0
- package/src/grok_client_ext.rs +177 -0
- package/src/hooks/loader.rs +407 -0
- package/src/hooks/mod.rs +158 -0
- package/src/lib.rs +174 -0
- package/src/main.rs +65 -0
- package/src/mcp/client.rs +195 -0
- package/src/mcp/config.rs +20 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +67 -0
- package/src/utils/auth.rs +41 -0
- package/src/utils/chat_logger.rs +568 -0
- package/src/utils/context.rs +390 -0
- package/src/utils/mod.rs +16 -0
- package/src/utils/network.rs +320 -0
- package/src/utils/rate_limiter.rs +166 -0
- package/src/utils/session.rs +73 -0
- package/src/utils/shell_permissions.rs +389 -0
- package/src/utils/telemetry.rs +41 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
# Error Handling Guide for grok-cli
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide documents the error handling patterns and best practices used throughout the grok-cli project. Following these patterns ensures consistency, maintainability, and robustness, especially when dealing with network instability on satellite connections like Starlink.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Core Principles](#core-principles)
|
|
10
|
+
2. [Error Types](#error-types)
|
|
11
|
+
3. [Error Handling Patterns](#error-handling-patterns)
|
|
12
|
+
4. [Network Error Handling](#network-error-handling)
|
|
13
|
+
5. [File I/O Error Handling](#file-io-error-handling)
|
|
14
|
+
6. [Testing Error Cases](#testing-error-cases)
|
|
15
|
+
7. [Common Pitfalls](#common-pitfalls)
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
### 1. Use Result<T, E> for Fallible Operations
|
|
20
|
+
|
|
21
|
+
Always return `Result` for operations that can fail:
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
use anyhow::Result;
|
|
25
|
+
|
|
26
|
+
pub fn load_config(path: &Path) -> Result<Config> {
|
|
27
|
+
let contents = fs::read_to_string(path)?;
|
|
28
|
+
let config: Config = toml::from_str(&contents)?;
|
|
29
|
+
Ok(config)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Propagate Errors with Context
|
|
34
|
+
|
|
35
|
+
Add context to errors as they bubble up:
|
|
36
|
+
|
|
37
|
+
```rust
|
|
38
|
+
use anyhow::Context;
|
|
39
|
+
|
|
40
|
+
pub fn save_session(session: &Session, path: &Path) -> Result<()> {
|
|
41
|
+
let json = serde_json::to_string_pretty(session)
|
|
42
|
+
.context("Failed to serialize session to JSON")?;
|
|
43
|
+
|
|
44
|
+
fs::write(path, json)
|
|
45
|
+
.with_context(|| format!("Failed to write session to {:?}", path))?;
|
|
46
|
+
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Never Unwrap in Production Code
|
|
52
|
+
|
|
53
|
+
Use `?` operator, `unwrap_or`, `unwrap_or_else`, or `expect` with descriptive messages:
|
|
54
|
+
|
|
55
|
+
```rust
|
|
56
|
+
// ❌ BAD - Can panic in production
|
|
57
|
+
let value = result.unwrap();
|
|
58
|
+
|
|
59
|
+
// ✅ GOOD - Propagates error
|
|
60
|
+
let value = result?;
|
|
61
|
+
|
|
62
|
+
// ✅ GOOD - Provides fallback
|
|
63
|
+
let value = result.unwrap_or_default();
|
|
64
|
+
|
|
65
|
+
// ✅ GOOD - Only for truly impossible cases
|
|
66
|
+
let value = mutex.lock()
|
|
67
|
+
.expect("Mutex poisoned - this is a critical bug");
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Log Errors Appropriately
|
|
71
|
+
|
|
72
|
+
Use structured logging to track errors:
|
|
73
|
+
|
|
74
|
+
```rust
|
|
75
|
+
use tracing::{error, warn, debug};
|
|
76
|
+
|
|
77
|
+
// Critical errors that prevent operation
|
|
78
|
+
error!("Failed to connect to API: {}", e);
|
|
79
|
+
|
|
80
|
+
// Recoverable issues
|
|
81
|
+
warn!("Failed to save cache, continuing without cache: {}", e);
|
|
82
|
+
|
|
83
|
+
// Diagnostic information
|
|
84
|
+
debug!("Retrying request after error: {}", e);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Error Types
|
|
88
|
+
|
|
89
|
+
### Custom Error Types with thiserror
|
|
90
|
+
|
|
91
|
+
Define custom error types for domain-specific errors:
|
|
92
|
+
|
|
93
|
+
```rust
|
|
94
|
+
use thiserror::Error;
|
|
95
|
+
|
|
96
|
+
#[derive(Debug, Error)]
|
|
97
|
+
pub enum GrokApiError {
|
|
98
|
+
#[error("Network error: {0}")]
|
|
99
|
+
Network(#[from] reqwest::Error),
|
|
100
|
+
|
|
101
|
+
#[error("Authentication failed: Invalid API key")]
|
|
102
|
+
Authentication,
|
|
103
|
+
|
|
104
|
+
#[error("Rate limit exceeded. Please try again later")]
|
|
105
|
+
RateLimit,
|
|
106
|
+
|
|
107
|
+
#[error("Model not found: {model}")]
|
|
108
|
+
ModelNotFound { model: String },
|
|
109
|
+
|
|
110
|
+
#[error("Request timeout after {timeout_secs} seconds")]
|
|
111
|
+
Timeout { timeout_secs: u64 },
|
|
112
|
+
|
|
113
|
+
#[error("Connection dropped - Network instability detected")]
|
|
114
|
+
NetworkDrop,
|
|
115
|
+
|
|
116
|
+
#[error("Maximum retries ({max_retries}) exceeded")]
|
|
117
|
+
MaxRetriesExceeded { max_retries: u32 },
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Using anyhow for Application Errors
|
|
122
|
+
|
|
123
|
+
Use `anyhow::Result` for application-level code where specific error types aren't needed:
|
|
124
|
+
|
|
125
|
+
```rust
|
|
126
|
+
use anyhow::{Result, anyhow};
|
|
127
|
+
|
|
128
|
+
pub fn validate_input(input: &str) -> Result<()> {
|
|
129
|
+
if input.is_empty() {
|
|
130
|
+
return Err(anyhow!("Input cannot be empty"));
|
|
131
|
+
}
|
|
132
|
+
Ok(())
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Error Handling Patterns
|
|
137
|
+
|
|
138
|
+
### Pattern 1: Retry with Exponential Backoff
|
|
139
|
+
|
|
140
|
+
For network operations that may fail temporarily:
|
|
141
|
+
|
|
142
|
+
```rust
|
|
143
|
+
pub async fn execute_with_retry<T, F, Fut>(
|
|
144
|
+
&self,
|
|
145
|
+
request_fn: F,
|
|
146
|
+
max_retries: u32,
|
|
147
|
+
) -> Result<T>
|
|
148
|
+
where
|
|
149
|
+
F: Fn() -> Fut,
|
|
150
|
+
Fut: std::future::Future<Output = Result<T>>,
|
|
151
|
+
{
|
|
152
|
+
let mut last_error = None;
|
|
153
|
+
|
|
154
|
+
for attempt in 1..=max_retries {
|
|
155
|
+
debug!("Attempt {} of {}", attempt, max_retries);
|
|
156
|
+
|
|
157
|
+
match request_fn().await {
|
|
158
|
+
Ok(result) => return Ok(result),
|
|
159
|
+
Err(e) => {
|
|
160
|
+
warn!("Attempt {} failed: {}", attempt, e);
|
|
161
|
+
last_error = Some(e);
|
|
162
|
+
|
|
163
|
+
if attempt < max_retries && should_retry(&e) {
|
|
164
|
+
let backoff = calculate_backoff(attempt);
|
|
165
|
+
debug!("Backing off for {:?}", backoff);
|
|
166
|
+
tokio::time::sleep(backoff).await;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Err(last_error.unwrap_or_else(|| anyhow!("All retries failed")))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn calculate_backoff(attempt: u32) -> Duration {
|
|
177
|
+
let base_delay = 2_u64.pow(attempt - 1);
|
|
178
|
+
let max_delay = 60;
|
|
179
|
+
let jitter = rand::random::<u64>() % 1000;
|
|
180
|
+
|
|
181
|
+
Duration::from_secs(base_delay.min(max_delay))
|
|
182
|
+
+ Duration::from_millis(jitter)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Pattern 2: Graceful Degradation
|
|
187
|
+
|
|
188
|
+
Continue operation with reduced functionality when non-critical components fail:
|
|
189
|
+
|
|
190
|
+
```rust
|
|
191
|
+
pub async fn initialize(&mut self) -> Result<()> {
|
|
192
|
+
// Critical: Must succeed
|
|
193
|
+
self.load_config().await
|
|
194
|
+
.context("Failed to load configuration")?;
|
|
195
|
+
|
|
196
|
+
// Non-critical: Log and continue
|
|
197
|
+
if let Err(e) = self.load_cache().await {
|
|
198
|
+
warn!("Failed to load cache: {}. Continuing without cache.", e);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Non-critical: Silent failure with fallback
|
|
202
|
+
self.stats = UsageStats::load().unwrap_or_default();
|
|
203
|
+
|
|
204
|
+
Ok(())
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Pattern 3: Resource Cleanup with Drop
|
|
209
|
+
|
|
210
|
+
Ensure resources are cleaned up even on error:
|
|
211
|
+
|
|
212
|
+
```rust
|
|
213
|
+
pub struct Session {
|
|
214
|
+
id: String,
|
|
215
|
+
// Drop automatically closes and saves
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
impl Drop for Session {
|
|
219
|
+
fn drop(&mut self) {
|
|
220
|
+
if let Err(e) = self.save() {
|
|
221
|
+
error!("Failed to save session {}: {}", self.id, e);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Pattern 4: Error Conversion
|
|
228
|
+
|
|
229
|
+
Convert between error types explicitly:
|
|
230
|
+
|
|
231
|
+
```rust
|
|
232
|
+
// Automatic conversion with From trait
|
|
233
|
+
impl From<std::io::Error> for MyError {
|
|
234
|
+
fn from(err: std::io::Error) -> Self {
|
|
235
|
+
MyError::Io(err)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Using in code
|
|
240
|
+
let file = File::open(path)?; // io::Error -> MyError automatically
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Network Error Handling
|
|
244
|
+
|
|
245
|
+
### Detecting Network Drops
|
|
246
|
+
|
|
247
|
+
The project includes Starlink-specific network drop detection:
|
|
248
|
+
|
|
249
|
+
```rust
|
|
250
|
+
pub fn detect_network_drop(error: &Error) -> bool {
|
|
251
|
+
let error_string = error.to_string().to_lowercase();
|
|
252
|
+
|
|
253
|
+
// Check for connection issues
|
|
254
|
+
error_string.contains("connection reset") ||
|
|
255
|
+
error_string.contains("broken pipe") ||
|
|
256
|
+
error_string.contains("network unreachable") ||
|
|
257
|
+
error_string.contains("timeout") ||
|
|
258
|
+
|
|
259
|
+
// Check for HTTP gateway errors
|
|
260
|
+
error_string.contains("502") ||
|
|
261
|
+
error_string.contains("503") ||
|
|
262
|
+
error_string.contains("504") ||
|
|
263
|
+
error_string.contains("520") ||
|
|
264
|
+
error_string.contains("521") ||
|
|
265
|
+
error_string.contains("522") ||
|
|
266
|
+
error_string.contains("523") ||
|
|
267
|
+
error_string.contains("524")
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### HTTP Client Configuration
|
|
272
|
+
|
|
273
|
+
Configure reqwest client with appropriate timeouts for satellite connections:
|
|
274
|
+
|
|
275
|
+
```rust
|
|
276
|
+
let client = ClientBuilder::new()
|
|
277
|
+
.timeout(Duration::from_secs(30)) // Total request timeout
|
|
278
|
+
.connect_timeout(Duration::from_secs(10)) // Connection timeout
|
|
279
|
+
.tcp_keepalive(Duration::from_secs(30)) // Keep connections alive
|
|
280
|
+
.pool_idle_timeout(Duration::from_secs(90))
|
|
281
|
+
.pool_max_idle_per_host(10)
|
|
282
|
+
.build()?;
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Retry Strategy for Network Errors
|
|
286
|
+
|
|
287
|
+
Determine which errors should trigger retries:
|
|
288
|
+
|
|
289
|
+
```rust
|
|
290
|
+
fn should_retry(error: &Error) -> bool {
|
|
291
|
+
let error_string = error.to_string().to_lowercase();
|
|
292
|
+
|
|
293
|
+
// Network-related errors
|
|
294
|
+
error_string.contains("connection") ||
|
|
295
|
+
error_string.contains("timeout") ||
|
|
296
|
+
error_string.contains("network") ||
|
|
297
|
+
error_string.contains("dns") ||
|
|
298
|
+
|
|
299
|
+
// Temporary server errors
|
|
300
|
+
error_string.contains("502") ||
|
|
301
|
+
error_string.contains("503") ||
|
|
302
|
+
error_string.contains("504")
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## File I/O Error Handling
|
|
307
|
+
|
|
308
|
+
### Safe File Writing
|
|
309
|
+
|
|
310
|
+
Always use atomic operations and error checking:
|
|
311
|
+
|
|
312
|
+
```rust
|
|
313
|
+
use std::io::Write;
|
|
314
|
+
use std::fs::OpenOptions;
|
|
315
|
+
|
|
316
|
+
pub fn save_to_file(path: &Path, content: &str) -> Result<()> {
|
|
317
|
+
// Create parent directory if needed
|
|
318
|
+
if let Some(parent) = path.parent() {
|
|
319
|
+
fs::create_dir_all(parent)
|
|
320
|
+
.with_context(|| format!("Failed to create directory {:?}", parent))?;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Write atomically
|
|
324
|
+
let mut file = OpenOptions::new()
|
|
325
|
+
.create(true)
|
|
326
|
+
.write(true)
|
|
327
|
+
.truncate(true)
|
|
328
|
+
.open(path)
|
|
329
|
+
.with_context(|| format!("Failed to open file {:?}", path))?;
|
|
330
|
+
|
|
331
|
+
file.write_all(content.as_bytes())
|
|
332
|
+
.with_context(|| format!("Failed to write to file {:?}", path))?;
|
|
333
|
+
|
|
334
|
+
file.sync_all()
|
|
335
|
+
.with_context(|| "Failed to sync file to disk")?;
|
|
336
|
+
|
|
337
|
+
Ok(())
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Reading Files with Validation
|
|
342
|
+
|
|
343
|
+
```rust
|
|
344
|
+
pub fn load_from_file(path: &Path) -> Result<Config> {
|
|
345
|
+
// Check if file exists
|
|
346
|
+
if !path.exists() {
|
|
347
|
+
return Err(anyhow!("Configuration file not found: {:?}", path));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Read and parse
|
|
351
|
+
let contents = fs::read_to_string(path)
|
|
352
|
+
.with_context(|| format!("Failed to read file {:?}", path))?;
|
|
353
|
+
|
|
354
|
+
let config: Config = toml::from_str(&contents)
|
|
355
|
+
.with_context(|| format!("Failed to parse TOML in {:?}", path))?;
|
|
356
|
+
|
|
357
|
+
// Validate
|
|
358
|
+
config.validate()
|
|
359
|
+
.context("Configuration validation failed")?;
|
|
360
|
+
|
|
361
|
+
Ok(config)
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Handling Missing Home Directory
|
|
366
|
+
|
|
367
|
+
Always provide fallbacks for environment-dependent paths:
|
|
368
|
+
|
|
369
|
+
```rust
|
|
370
|
+
pub fn get_config_dir() -> Result<PathBuf> {
|
|
371
|
+
let home = dirs::home_dir()
|
|
372
|
+
.ok_or_else(|| anyhow!("Could not determine home directory"))?;
|
|
373
|
+
|
|
374
|
+
Ok(home.join(".grok"))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Or with fallback
|
|
378
|
+
pub fn get_config_dir_with_fallback() -> PathBuf {
|
|
379
|
+
dirs::home_dir()
|
|
380
|
+
.unwrap_or_else(|| PathBuf::from("."))
|
|
381
|
+
.join(".grok")
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Testing Error Cases
|
|
386
|
+
|
|
387
|
+
### Unit Tests for Error Handling
|
|
388
|
+
|
|
389
|
+
Test both success and failure paths:
|
|
390
|
+
|
|
391
|
+
```rust
|
|
392
|
+
#[cfg(test)]
|
|
393
|
+
mod tests {
|
|
394
|
+
use super::*;
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn test_valid_config() {
|
|
398
|
+
let config = Config::default();
|
|
399
|
+
assert!(config.validate().is_ok());
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[test]
|
|
403
|
+
fn test_invalid_temperature() {
|
|
404
|
+
let config = Config {
|
|
405
|
+
temperature: -1.0, // Invalid
|
|
406
|
+
..Default::default()
|
|
407
|
+
};
|
|
408
|
+
assert!(config.validate().is_err());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#[test]
|
|
412
|
+
fn test_network_drop_detection() {
|
|
413
|
+
assert!(detect_network_drop(&anyhow!("Connection reset by peer")));
|
|
414
|
+
assert!(detect_network_drop(&anyhow!("HTTP 502 Bad Gateway")));
|
|
415
|
+
assert!(!detect_network_drop(&anyhow!("Invalid API key")));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Integration Tests with Mocking
|
|
421
|
+
|
|
422
|
+
Use `mockito` for HTTP error simulation:
|
|
423
|
+
|
|
424
|
+
```rust
|
|
425
|
+
#[cfg(test)]
|
|
426
|
+
mod integration_tests {
|
|
427
|
+
use mockito::{mock, server_url};
|
|
428
|
+
|
|
429
|
+
#[tokio::test]
|
|
430
|
+
async fn test_retry_on_timeout() {
|
|
431
|
+
let _m = mock("POST", "/api/chat")
|
|
432
|
+
.with_status(504) // Gateway Timeout
|
|
433
|
+
.expect_at_least(2) // Should retry
|
|
434
|
+
.create();
|
|
435
|
+
|
|
436
|
+
let client = GrokClient::new("test-key")
|
|
437
|
+
.unwrap()
|
|
438
|
+
.with_base_url(server_url());
|
|
439
|
+
|
|
440
|
+
let result = client.chat_completion("test", None, 0.7, 100, "grok-2").await;
|
|
441
|
+
|
|
442
|
+
// Should fail after retries
|
|
443
|
+
assert!(result.is_err());
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Common Pitfalls
|
|
449
|
+
|
|
450
|
+
### ❌ Pitfall 1: Silent Failures
|
|
451
|
+
|
|
452
|
+
```rust
|
|
453
|
+
// BAD: Error is ignored
|
|
454
|
+
let _ = save_to_file(&path, content);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```rust
|
|
458
|
+
// GOOD: Log the error
|
|
459
|
+
if let Err(e) = save_to_file(&path, content) {
|
|
460
|
+
warn!("Failed to save to file: {}. Continuing without save.", e);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### ❌ Pitfall 2: Generic Error Messages
|
|
465
|
+
|
|
466
|
+
```rust
|
|
467
|
+
// BAD: No context
|
|
468
|
+
fs::read_to_string(path)?;
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```rust
|
|
472
|
+
// GOOD: Descriptive error
|
|
473
|
+
fs::read_to_string(path)
|
|
474
|
+
.with_context(|| format!("Failed to read config from {:?}", path))?;
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### ❌ Pitfall 3: Unwrapping in Production
|
|
478
|
+
|
|
479
|
+
```rust
|
|
480
|
+
// BAD: Can panic
|
|
481
|
+
let config = load_config().unwrap();
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
```rust
|
|
485
|
+
// GOOD: Handle error gracefully
|
|
486
|
+
let config = load_config()
|
|
487
|
+
.unwrap_or_else(|e| {
|
|
488
|
+
warn!("Failed to load config: {}. Using defaults.", e);
|
|
489
|
+
Config::default()
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### ❌ Pitfall 4: Not Retrying Network Errors
|
|
494
|
+
|
|
495
|
+
```rust
|
|
496
|
+
// BAD: Single attempt
|
|
497
|
+
let response = client.post(url).send().await?;
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
```rust
|
|
501
|
+
// GOOD: Retry with backoff
|
|
502
|
+
let response = execute_with_retry(|| async {
|
|
503
|
+
client.post(url).send().await
|
|
504
|
+
}).await?;
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### ❌ Pitfall 5: Mutex Unwrap Without Message
|
|
508
|
+
|
|
509
|
+
```rust
|
|
510
|
+
// BAD: Panic with no information
|
|
511
|
+
let data = mutex.lock().unwrap();
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
```rust
|
|
515
|
+
// GOOD: Descriptive message for debugging
|
|
516
|
+
let data = mutex.lock()
|
|
517
|
+
.expect("Mutex poisoned - this indicates a panic in another thread");
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Checklist for New Code
|
|
521
|
+
|
|
522
|
+
When adding new error-prone code, verify:
|
|
523
|
+
|
|
524
|
+
- [ ] All fallible operations return `Result<T, E>`
|
|
525
|
+
- [ ] Errors have descriptive messages with context
|
|
526
|
+
- [ ] No `.unwrap()` calls in production code paths
|
|
527
|
+
- [ ] Network operations have retry logic
|
|
528
|
+
- [ ] File operations check for directory existence
|
|
529
|
+
- [ ] Errors are logged at appropriate levels
|
|
530
|
+
- [ ] Critical failures stop execution
|
|
531
|
+
- [ ] Non-critical failures degrade gracefully
|
|
532
|
+
- [ ] Tests cover both success and error paths
|
|
533
|
+
- [ ] Documentation mentions error conditions
|
|
534
|
+
|
|
535
|
+
## Resources
|
|
536
|
+
|
|
537
|
+
- [anyhow documentation](https://docs.rs/anyhow/)
|
|
538
|
+
- [thiserror documentation](https://docs.rs/thiserror/)
|
|
539
|
+
- [Rust Error Handling](https://doc.rust-lang.org/book/ch09-00-error-handling.html)
|
|
540
|
+
- Project: `src/api/mod.rs` - Network error handling examples
|
|
541
|
+
- Project: `src/utils/chat_logger.rs` - File I/O error handling examples
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
**Last Updated:** 2025-01
|
|
546
|
+
**Maintainers:** grok-cli development team
|
|
547
|
+
**Questions:** Open an issue on GitHub
|