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.
Files changed (100) hide show
  1. package/.env.example +42 -0
  2. package/.github/workflows/ci.yml +30 -0
  3. package/.github/workflows/rust.yml +22 -0
  4. package/.grok/.env.example +85 -0
  5. package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
  6. package/.grok/ENV_CONFIG_GUIDE.md +173 -0
  7. package/.grok/QUICK_REFERENCE.md +180 -0
  8. package/.grok/README.md +104 -0
  9. package/.grok/TESTING_GUIDE.md +393 -0
  10. package/CHANGELOG.md +465 -0
  11. package/CODE_REVIEW_SUMMARY.md +414 -0
  12. package/COMPLETE_FIX_SUMMARY.md +415 -0
  13. package/CONFIGURATION.md +489 -0
  14. package/CONTEXT_FILES_GUIDE.md +419 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/CURSOR_POSITION_FIX.md +206 -0
  17. package/Cargo.toml +88 -0
  18. package/ERROR_HANDLING_REPORT.md +361 -0
  19. package/FINAL_FIX_SUMMARY.md +462 -0
  20. package/FIXES.md +37 -0
  21. package/FIXES_SUMMARY.md +87 -0
  22. package/GROK_API_MIGRATION_SUMMARY.md +111 -0
  23. package/LICENSE +22 -0
  24. package/MIGRATION_TO_GROK_API.md +223 -0
  25. package/README.md +504 -0
  26. package/REVIEW_COMPLETE.md +416 -0
  27. package/REVIEW_QUICK_REFERENCE.md +173 -0
  28. package/SECURITY.md +463 -0
  29. package/SECURITY_AUDIT.md +661 -0
  30. package/SETUP.md +287 -0
  31. package/TESTING_TOOLS.md +88 -0
  32. package/TESTING_TOOL_EXECUTION.md +239 -0
  33. package/TOOL_EXECUTION_FIX.md +491 -0
  34. package/VERIFICATION_CHECKLIST.md +419 -0
  35. package/docs/API.md +74 -0
  36. package/docs/CHAT_LOGGING.md +39 -0
  37. package/docs/CURSOR_FIX_DEMO.md +306 -0
  38. package/docs/ERROR_HANDLING_GUIDE.md +547 -0
  39. package/docs/FILE_OPERATIONS.md +449 -0
  40. package/docs/INTERACTIVE.md +401 -0
  41. package/docs/PROJECT_CREATION_GUIDE.md +570 -0
  42. package/docs/QUICKSTART.md +378 -0
  43. package/docs/QUICK_REFERENCE.md +691 -0
  44. package/docs/RELEASE_NOTES_0.1.2.md +240 -0
  45. package/docs/TOOLS.md +459 -0
  46. package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
  47. package/docs/ZED_INTEGRATION.md +371 -0
  48. package/docs/extensions.md +464 -0
  49. package/docs/settings.md +293 -0
  50. package/examples/extensions/logging-hook/README.md +91 -0
  51. package/examples/extensions/logging-hook/extension.json +22 -0
  52. package/package.json +30 -0
  53. package/scripts/test_acp.py +252 -0
  54. package/scripts/test_acp.sh +143 -0
  55. package/scripts/test_acp_simple.sh +72 -0
  56. package/src/acp/mod.rs +741 -0
  57. package/src/acp/protocol.rs +323 -0
  58. package/src/acp/security.rs +298 -0
  59. package/src/acp/tools.rs +697 -0
  60. package/src/bin/banner_demo.rs +216 -0
  61. package/src/bin/docgen.rs +18 -0
  62. package/src/bin/installer.rs +217 -0
  63. package/src/cli/app.rs +310 -0
  64. package/src/cli/commands/acp.rs +721 -0
  65. package/src/cli/commands/chat.rs +485 -0
  66. package/src/cli/commands/code.rs +513 -0
  67. package/src/cli/commands/config.rs +394 -0
  68. package/src/cli/commands/health.rs +442 -0
  69. package/src/cli/commands/history.rs +421 -0
  70. package/src/cli/commands/mod.rs +14 -0
  71. package/src/cli/commands/settings.rs +1384 -0
  72. package/src/cli/mod.rs +166 -0
  73. package/src/config/mod.rs +2212 -0
  74. package/src/display/ascii_art.rs +139 -0
  75. package/src/display/banner.rs +289 -0
  76. package/src/display/components/input.rs +323 -0
  77. package/src/display/components/mod.rs +2 -0
  78. package/src/display/components/settings_list.rs +306 -0
  79. package/src/display/interactive.rs +1255 -0
  80. package/src/display/mod.rs +62 -0
  81. package/src/display/terminal.rs +42 -0
  82. package/src/display/tips.rs +316 -0
  83. package/src/grok_client_ext.rs +177 -0
  84. package/src/hooks/loader.rs +407 -0
  85. package/src/hooks/mod.rs +158 -0
  86. package/src/lib.rs +174 -0
  87. package/src/main.rs +65 -0
  88. package/src/mcp/client.rs +195 -0
  89. package/src/mcp/config.rs +20 -0
  90. package/src/mcp/mod.rs +6 -0
  91. package/src/mcp/protocol.rs +67 -0
  92. package/src/utils/auth.rs +41 -0
  93. package/src/utils/chat_logger.rs +568 -0
  94. package/src/utils/context.rs +390 -0
  95. package/src/utils/mod.rs +16 -0
  96. package/src/utils/network.rs +320 -0
  97. package/src/utils/rate_limiter.rs +166 -0
  98. package/src/utils/session.rs +73 -0
  99. package/src/utils/shell_permissions.rs +389 -0
  100. package/src/utils/telemetry.rs +41 -0
@@ -0,0 +1,568 @@
1
+ //! Chat session logging module
2
+ //!
3
+ //! Provides comprehensive logging of chat sessions including:
4
+ //! - User prompts and assistant responses
5
+ //! - Session metadata and timestamps
6
+ //! - Multiple output formats (JSON, text)
7
+ //! - Automatic file rotation and management
8
+ //! - Robust error handling for network/disk issues
9
+
10
+ use anyhow::{Context, Result};
11
+ use chrono::{DateTime, Utc};
12
+ use serde::{Deserialize, Serialize};
13
+ use std::fs::{self, File, OpenOptions};
14
+ use std::io::Write;
15
+ use std::path::{Path, PathBuf};
16
+ use std::sync::Mutex;
17
+ use tracing::{debug, error, info, warn};
18
+
19
+ /// Configuration for chat logging
20
+ #[derive(Debug, Clone)]
21
+ pub struct ChatLoggerConfig {
22
+ /// Enable chat logging
23
+ pub enabled: bool,
24
+ /// Directory to store chat logs
25
+ pub log_dir: PathBuf,
26
+ /// Enable JSON format
27
+ pub json_format: bool,
28
+ /// Enable human-readable text format
29
+ pub text_format: bool,
30
+ /// Maximum log file size in MB before rotation
31
+ pub max_file_size_mb: u64,
32
+ /// Number of rotated files to keep
33
+ pub rotation_count: usize,
34
+ /// Include system messages in logs
35
+ pub include_system: bool,
36
+ }
37
+
38
+ impl Default for ChatLoggerConfig {
39
+ fn default() -> Self {
40
+ let log_dir = dirs::home_dir()
41
+ .unwrap_or_else(|| PathBuf::from("."))
42
+ .join(".grok")
43
+ .join("logs")
44
+ .join("chat_sessions");
45
+
46
+ Self {
47
+ enabled: true,
48
+ log_dir,
49
+ json_format: true,
50
+ text_format: true,
51
+ max_file_size_mb: 10,
52
+ rotation_count: 5,
53
+ include_system: true,
54
+ }
55
+ }
56
+ }
57
+
58
+ /// Represents a single chat message
59
+ #[derive(Debug, Clone, Serialize, Deserialize)]
60
+ pub struct ChatMessage {
61
+ /// Timestamp of the message
62
+ pub timestamp: DateTime<Utc>,
63
+ /// Role: "user", "assistant", or "system"
64
+ pub role: String,
65
+ /// Message content
66
+ pub content: String,
67
+ /// Optional metadata (model used, tokens, etc.)
68
+ #[serde(skip_serializing_if = "Option::is_none")]
69
+ pub metadata: Option<serde_json::Value>,
70
+ }
71
+
72
+ impl ChatMessage {
73
+ pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
74
+ Self {
75
+ timestamp: Utc::now(),
76
+ role: role.into(),
77
+ content: content.into(),
78
+ metadata: None,
79
+ }
80
+ }
81
+
82
+ pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
83
+ self.metadata = Some(metadata);
84
+ self
85
+ }
86
+ }
87
+
88
+ /// Represents a complete chat session
89
+ #[derive(Debug, Clone, Serialize, Deserialize)]
90
+ pub struct ChatSession {
91
+ /// Unique session identifier
92
+ pub session_id: String,
93
+ /// Session start time
94
+ pub start_time: DateTime<Utc>,
95
+ /// Session end time (if completed)
96
+ #[serde(skip_serializing_if = "Option::is_none")]
97
+ pub end_time: Option<DateTime<Utc>>,
98
+ /// All messages in the session
99
+ pub messages: Vec<ChatMessage>,
100
+ /// Session metadata
101
+ #[serde(skip_serializing_if = "Option::is_none")]
102
+ pub metadata: Option<serde_json::Value>,
103
+ }
104
+
105
+ impl ChatSession {
106
+ pub fn new(session_id: impl Into<String>) -> Self {
107
+ Self {
108
+ session_id: session_id.into(),
109
+ start_time: Utc::now(),
110
+ end_time: None,
111
+ messages: Vec::new(),
112
+ metadata: None,
113
+ }
114
+ }
115
+
116
+ pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
117
+ self.metadata = Some(metadata);
118
+ self
119
+ }
120
+
121
+ pub fn add_message(&mut self, message: ChatMessage) {
122
+ self.messages.push(message);
123
+ }
124
+
125
+ pub fn end_session(&mut self) {
126
+ self.end_time = Some(Utc::now());
127
+ }
128
+ }
129
+
130
+ /// Chat logger instance
131
+ pub struct ChatLogger {
132
+ config: ChatLoggerConfig,
133
+ current_session: Mutex<Option<ChatSession>>,
134
+ }
135
+
136
+ impl ChatLogger {
137
+ /// Create a new chat logger with the given configuration
138
+ pub fn new(config: ChatLoggerConfig) -> Result<Self> {
139
+ // Ensure log directory exists
140
+ if config.enabled {
141
+ fs::create_dir_all(&config.log_dir)
142
+ .with_context(|| format!("Failed to create log directory: {:?}", config.log_dir))?;
143
+ info!("Chat logger initialized: {:?}", config.log_dir);
144
+ }
145
+
146
+ Ok(Self {
147
+ config,
148
+ current_session: Mutex::new(None),
149
+ })
150
+ }
151
+
152
+ /// Start a new chat session
153
+ pub fn start_session(&self, session_id: impl Into<String>) -> Result<()> {
154
+ if !self.config.enabled {
155
+ return Ok(());
156
+ }
157
+
158
+ let session_id = session_id.into();
159
+ let mut current = self.current_session.lock().unwrap();
160
+
161
+ // End previous session if exists
162
+ if let Some(prev_session) = current.take() {
163
+ warn!(
164
+ "Starting new session {} while session {} was active. Ending previous session.",
165
+ session_id, prev_session.session_id
166
+ );
167
+ drop(current); // Release lock before saving
168
+ self.save_session(&prev_session)?;
169
+ current = self.current_session.lock().unwrap();
170
+ }
171
+
172
+ let session = ChatSession::new(&session_id);
173
+ info!("Started chat session: {}", session_id);
174
+ *current = Some(session);
175
+
176
+ Ok(())
177
+ }
178
+
179
+ /// Log a user prompt
180
+ pub fn log_user_message(&self, content: impl Into<String>) -> Result<()> {
181
+ self.log_message("user", content, None)
182
+ }
183
+
184
+ /// Log an assistant response
185
+ pub fn log_assistant_message(&self, content: impl Into<String>) -> Result<()> {
186
+ self.log_message("assistant", content, None)
187
+ }
188
+
189
+ /// Log a system message
190
+ pub fn log_system_message(&self, content: impl Into<String>) -> Result<()> {
191
+ if !self.config.include_system {
192
+ return Ok(());
193
+ }
194
+ self.log_message("system", content, None)
195
+ }
196
+
197
+ /// Log a message with optional metadata
198
+ pub fn log_message(
199
+ &self,
200
+ role: impl Into<String>,
201
+ content: impl Into<String>,
202
+ metadata: Option<serde_json::Value>,
203
+ ) -> Result<()> {
204
+ if !self.config.enabled {
205
+ return Ok(());
206
+ }
207
+
208
+ let mut current = self.current_session.lock().unwrap();
209
+ if let Some(session) = current.as_mut() {
210
+ let mut message = ChatMessage::new(role, content);
211
+ if let Some(meta) = metadata {
212
+ message = message.with_metadata(meta);
213
+ }
214
+ debug!(
215
+ "Logging message in session {}: {}",
216
+ session.session_id, message.role
217
+ );
218
+ session.add_message(message);
219
+ } else {
220
+ warn!("Attempted to log message without an active session");
221
+ }
222
+
223
+ Ok(())
224
+ }
225
+
226
+ /// End the current session and save it
227
+ pub fn end_session(&self) -> Result<()> {
228
+ if !self.config.enabled {
229
+ return Ok(());
230
+ }
231
+
232
+ let mut current = self.current_session.lock().unwrap();
233
+ if let Some(mut session) = current.take() {
234
+ session.end_session();
235
+ let session_id = session.session_id.clone();
236
+ drop(current); // Release lock before saving
237
+ self.save_session(&session)?;
238
+ info!("Ended chat session: {}", session_id);
239
+ }
240
+
241
+ Ok(())
242
+ }
243
+
244
+ /// Save a session to disk
245
+ fn save_session(&self, session: &ChatSession) -> Result<()> {
246
+ let base_path = self.config.log_dir.join(&session.session_id);
247
+
248
+ // Save JSON format
249
+ if self.config.json_format {
250
+ let json_path = base_path.with_extension("json");
251
+ self.save_json(session, &json_path)
252
+ .with_context(|| format!("Failed to save JSON log: {:?}", json_path))?;
253
+ }
254
+
255
+ // Save text format
256
+ if self.config.text_format {
257
+ let text_path = base_path.with_extension("txt");
258
+ self.save_text(session, &text_path)
259
+ .with_context(|| format!("Failed to save text log: {:?}", text_path))?;
260
+ }
261
+
262
+ // Check and rotate if needed
263
+ self.rotate_logs_if_needed()?;
264
+
265
+ Ok(())
266
+ }
267
+
268
+ /// Save session as JSON
269
+ fn save_json(&self, session: &ChatSession, path: &Path) -> Result<()> {
270
+ let json = serde_json::to_string_pretty(session)
271
+ .with_context(|| "Failed to serialize session to JSON")?;
272
+
273
+ let mut file = OpenOptions::new()
274
+ .create(true)
275
+ .write(true)
276
+ .truncate(true)
277
+ .open(path)
278
+ .with_context(|| format!("Failed to open file: {:?}", path))?;
279
+
280
+ file.write_all(json.as_bytes())
281
+ .with_context(|| format!("Failed to write JSON to file: {:?}", path))?;
282
+
283
+ debug!("Saved JSON log: {:?}", path);
284
+ Ok(())
285
+ }
286
+
287
+ /// Save session as human-readable text
288
+ fn save_text(&self, session: &ChatSession, path: &Path) -> Result<()> {
289
+ let mut file = OpenOptions::new()
290
+ .create(true)
291
+ .write(true)
292
+ .truncate(true)
293
+ .open(path)
294
+ .with_context(|| format!("Failed to open file: {:?}", path))?;
295
+
296
+ // Write header
297
+ let separator = "=".repeat(80);
298
+ writeln!(file, "{}", separator)?;
299
+ writeln!(file, "GROK CLI CHAT SESSION LOG")?;
300
+ writeln!(file, "{}", separator)?;
301
+ writeln!(file, "Session ID: {}", session.session_id)?;
302
+ writeln!(
303
+ file,
304
+ "Start Time: {}",
305
+ session.start_time.format("%Y-%m-%d %H:%M:%S UTC")
306
+ )?;
307
+ if let Some(end_time) = session.end_time {
308
+ writeln!(
309
+ file,
310
+ "End Time: {}",
311
+ end_time.format("%Y-%m-%d %H:%M:%S UTC")
312
+ )?;
313
+ let duration = end_time.signed_duration_since(session.start_time);
314
+ writeln!(file, "Duration: {} seconds", duration.num_seconds())?;
315
+ }
316
+ writeln!(file, "Messages: {}", session.messages.len())?;
317
+ let separator = "=".repeat(80);
318
+ writeln!(file, "{}", separator)?;
319
+ writeln!(file)?;
320
+
321
+ // Write messages
322
+ for (i, msg) in session.messages.iter().enumerate() {
323
+ writeln!(
324
+ file,
325
+ "[{}] {} - {}",
326
+ i + 1,
327
+ msg.role.to_uppercase(),
328
+ msg.timestamp.format("%H:%M:%S")
329
+ )?;
330
+ let line_sep = "-".repeat(80);
331
+ writeln!(file, "{}", line_sep)?;
332
+ writeln!(file, "{}", msg.content)?;
333
+
334
+ if let Some(metadata) = &msg.metadata {
335
+ writeln!(
336
+ file,
337
+ "\nMetadata: {}",
338
+ serde_json::to_string_pretty(metadata).unwrap_or_default()
339
+ )?;
340
+ }
341
+
342
+ writeln!(file)?;
343
+ }
344
+
345
+ // Write footer
346
+ let separator = "=".repeat(80);
347
+ writeln!(file, "{}", separator)?;
348
+ writeln!(file, "END OF SESSION")?;
349
+ writeln!(file, "{}", separator)?;
350
+
351
+ debug!("Saved text log: {:?}", path);
352
+ Ok(())
353
+ }
354
+
355
+ /// Rotate logs if they exceed the size limit
356
+ fn rotate_logs_if_needed(&self) -> Result<()> {
357
+ let max_bytes = self.config.max_file_size_mb * 1024 * 1024;
358
+
359
+ // Get all log files
360
+ let entries = match fs::read_dir(&self.config.log_dir) {
361
+ Ok(entries) => entries,
362
+ Err(e) => {
363
+ warn!("Failed to read log directory for rotation: {}", e);
364
+ return Ok(()); // Don't fail on rotation issues
365
+ }
366
+ };
367
+
368
+ let mut total_size: u64 = 0;
369
+ let mut files: Vec<(PathBuf, u64)> = Vec::new();
370
+
371
+ for entry in entries.flatten() {
372
+ if let Ok(metadata) = entry.metadata()
373
+ && metadata.is_file()
374
+ {
375
+ let size = metadata.len();
376
+ total_size += size;
377
+ files.push((entry.path(), size));
378
+ }
379
+ }
380
+
381
+ // If total size exceeds limit, remove oldest files
382
+ if total_size > max_bytes {
383
+ // Sort by modification time (oldest first)
384
+ files.sort_by_key(|(path, _)| {
385
+ fs::metadata(path)
386
+ .and_then(|m| m.modified())
387
+ .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
388
+ });
389
+
390
+ // Calculate how many to remove
391
+ let files_to_keep = files.len().saturating_sub(self.config.rotation_count);
392
+
393
+ for (path, _) in files.iter().take(files_to_keep) {
394
+ if let Err(e) = fs::remove_file(path) {
395
+ warn!("Failed to remove old log file {:?}: {}", path, e);
396
+ } else {
397
+ debug!("Rotated old log file: {:?}", path);
398
+ }
399
+ }
400
+ }
401
+
402
+ Ok(())
403
+ }
404
+
405
+ /// List all saved sessions
406
+ pub fn list_sessions(&self) -> Result<Vec<String>> {
407
+ let mut sessions = Vec::new();
408
+
409
+ let entries = fs::read_dir(&self.config.log_dir)
410
+ .with_context(|| format!("Failed to read log directory: {:?}", self.config.log_dir))?;
411
+
412
+ for entry in entries.flatten() {
413
+ let path = entry.path();
414
+ if path.extension().and_then(|s| s.to_str()) == Some("json")
415
+ && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
416
+ {
417
+ sessions.push(stem.to_string());
418
+ }
419
+ }
420
+
421
+ sessions.sort();
422
+ sessions.reverse(); // Most recent first
423
+
424
+ Ok(sessions)
425
+ }
426
+
427
+ /// Load a session from disk
428
+ pub fn load_session(&self, session_id: &str) -> Result<ChatSession> {
429
+ let json_path = self.config.log_dir.join(session_id).with_extension("json");
430
+
431
+ let content = fs::read_to_string(&json_path)
432
+ .with_context(|| format!("Failed to read session file: {:?}", json_path))?;
433
+
434
+ let session: ChatSession = serde_json::from_str(&content)
435
+ .with_context(|| format!("Failed to parse session JSON: {:?}", json_path))?;
436
+
437
+ Ok(session)
438
+ }
439
+ }
440
+
441
+ /// Global chat logger instance
442
+ static GLOBAL_LOGGER: Mutex<Option<ChatLogger>> = Mutex::new(None);
443
+
444
+ /// Initialize the global chat logger
445
+ pub fn init(config: ChatLoggerConfig) -> Result<()> {
446
+ let logger = ChatLogger::new(config)?;
447
+ let mut global = GLOBAL_LOGGER.lock().unwrap();
448
+ *global = Some(logger);
449
+ Ok(())
450
+ }
451
+
452
+ /// Get a reference to the global logger (if initialized)
453
+ pub fn get_logger() -> Option<ChatLogger> {
454
+ let global = GLOBAL_LOGGER.lock().unwrap();
455
+ global.as_ref().map(|logger| ChatLogger {
456
+ config: logger.config.clone(),
457
+ current_session: Mutex::new(None),
458
+ })
459
+ }
460
+
461
+ /// Start a new session using the global logger
462
+ pub fn start_session(session_id: impl Into<String>) -> Result<()> {
463
+ let global = GLOBAL_LOGGER.lock().unwrap();
464
+ if let Some(logger) = global.as_ref() {
465
+ logger.start_session(session_id)?;
466
+ }
467
+ Ok(())
468
+ }
469
+
470
+ /// Log a user message using the global logger
471
+ pub fn log_user(content: impl Into<String>) -> Result<()> {
472
+ let global = GLOBAL_LOGGER.lock().unwrap();
473
+ if let Some(logger) = global.as_ref() {
474
+ logger.log_user_message(content)?;
475
+ }
476
+ Ok(())
477
+ }
478
+
479
+ /// Log an assistant message using the global logger
480
+ pub fn log_assistant(content: impl Into<String>) -> Result<()> {
481
+ let global = GLOBAL_LOGGER.lock().unwrap();
482
+ if let Some(logger) = global.as_ref() {
483
+ logger.log_assistant_message(content)?;
484
+ }
485
+ Ok(())
486
+ }
487
+
488
+ /// Log a system message using the global logger
489
+ pub fn log_system(content: impl Into<String>) -> Result<()> {
490
+ let global = GLOBAL_LOGGER.lock().unwrap();
491
+ if let Some(logger) = global.as_ref() {
492
+ logger.log_system_message(content)?;
493
+ }
494
+ Ok(())
495
+ }
496
+
497
+ /// End the current session using the global logger
498
+ pub fn end_session() -> Result<()> {
499
+ let global = GLOBAL_LOGGER.lock().unwrap();
500
+ if let Some(logger) = global.as_ref() {
501
+ logger.end_session()?;
502
+ }
503
+ Ok(())
504
+ }
505
+
506
+ #[cfg(test)]
507
+ mod tests {
508
+ use super::*;
509
+ use tempfile::TempDir;
510
+
511
+ #[test]
512
+ fn test_chat_session_creation() {
513
+ let session = ChatSession::new("test-session-123");
514
+ assert_eq!(session.session_id, "test-session-123");
515
+ assert!(session.messages.is_empty());
516
+ assert!(session.end_time.is_none());
517
+ }
518
+
519
+ #[test]
520
+ fn test_chat_message_creation() {
521
+ let msg = ChatMessage::new("user", "Hello, world!");
522
+ assert_eq!(msg.role, "user");
523
+ assert_eq!(msg.content, "Hello, world!");
524
+ assert!(msg.metadata.is_none());
525
+ }
526
+
527
+ #[test]
528
+ fn test_session_add_message() {
529
+ let mut session = ChatSession::new("test-session");
530
+ session.add_message(ChatMessage::new("user", "Hello"));
531
+ session.add_message(ChatMessage::new("assistant", "Hi there!"));
532
+ assert_eq!(session.messages.len(), 2);
533
+ }
534
+
535
+ #[test]
536
+ fn test_logger_initialization() {
537
+ let temp_dir = TempDir::new().unwrap();
538
+ let config = ChatLoggerConfig {
539
+ enabled: true,
540
+ log_dir: temp_dir.path().to_path_buf(),
541
+ ..Default::default()
542
+ };
543
+
544
+ let logger = ChatLogger::new(config).unwrap();
545
+ assert!(temp_dir.path().exists());
546
+ }
547
+
548
+ #[test]
549
+ fn test_session_lifecycle() {
550
+ let temp_dir = TempDir::new().unwrap();
551
+ let config = ChatLoggerConfig {
552
+ enabled: true,
553
+ log_dir: temp_dir.path().to_path_buf(),
554
+ ..Default::default()
555
+ };
556
+
557
+ let logger = ChatLogger::new(config).unwrap();
558
+
559
+ logger.start_session("test-lifecycle").unwrap();
560
+ logger.log_user_message("Test message").unwrap();
561
+ logger.log_assistant_message("Test response").unwrap();
562
+ logger.end_session().unwrap();
563
+
564
+ // Check that files were created
565
+ assert!(temp_dir.path().join("test-lifecycle.json").exists());
566
+ assert!(temp_dir.path().join("test-lifecycle.txt").exists());
567
+ }
568
+ }