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