Skip to main content

vtcode_core/audit/
file_conflict_log.rs

1//! Audit logging for externally modified file conflicts.
2
3use crate::utils::error_messages::ERR_CREATE_AUDIT_DIR;
4use crate::utils::file_utils::ensure_dir_exists_sync;
5use anyhow::{Context, Result};
6use chrono::{DateTime, Local};
7use serde::{Deserialize, Serialize};
8use std::fs::OpenOptions;
9use std::io::BufWriter;
10use std::path::{Path, PathBuf};
11
12/// Audit event emitted when a tracked file changes outside VT Code control.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FileConflictAuditEvent {
15    pub timestamp: DateTime<Local>,
16    pub path: PathBuf,
17    pub reason: String,
18    pub file_exists: bool,
19    pub size_bytes: Option<u64>,
20    pub sha256: Option<String>,
21}
22
23/// JSONL audit logger for file-conflict events.
24pub struct FileConflictAuditLog {
25    writer: Option<BufWriter<std::fs::File>>,
26    log_path: PathBuf,
27}
28
29impl FileConflictAuditLog {
30    /// Create or open today's file-conflict audit log in the given directory.
31    pub fn new(audit_dir: PathBuf) -> Result<Self> {
32        ensure_dir_exists_sync(&audit_dir).context(ERR_CREATE_AUDIT_DIR)?;
33
34        let date = Local::now().format("%Y-%m-%d");
35        let log_path = audit_dir.join(format!("file-conflicts-{}.log", date));
36        Ok(Self {
37            writer: None,
38            log_path,
39        })
40    }
41
42    pub fn record(&mut self, event: &FileConflictAuditEvent) -> Result<()> {
43        use std::io::Write;
44
45        let json = serde_json::to_string(event)
46            .context("Failed to serialize file conflict audit event")?;
47        let writer = self.writer_mut()?;
48        writeln!(writer, "{json}").context("Failed to write file conflict audit event")?;
49        writer
50            .flush()
51            .context("Failed to flush file conflict audit log")?;
52        Ok(())
53    }
54
55    pub fn log_path(&self) -> &Path {
56        &self.log_path
57    }
58
59    fn writer_mut(&mut self) -> Result<&mut BufWriter<std::fs::File>> {
60        if self.writer.is_none() {
61            let file = OpenOptions::new()
62                .create(true)
63                .append(true)
64                .open(&self.log_path)
65                .with_context(|| {
66                    format!(
67                        "Failed to open file conflict audit log at {:?}",
68                        self.log_path
69                    )
70                })?;
71            self.writer = Some(BufWriter::new(file));
72        }
73
74        self.writer
75            .as_mut()
76            .context("file conflict audit log writer was not initialized")
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use tempfile::TempDir;
84
85    #[test]
86    fn defers_file_conflict_audit_log_creation_until_first_record() -> Result<()> {
87        let dir = TempDir::new()?;
88        let log = FileConflictAuditLog::new(dir.path().to_path_buf())?;
89        assert!(!log.log_path().exists());
90        Ok(())
91    }
92}